Spaces:
Runtime error
Runtime error
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +6 -0
- .gitignore +56 -0
- .npmrc +2 -0
- .prettierrc +4 -0
- Dockerfile +23 -0
- ENV_VARIABLES +11 -0
- README.md +9 -5
- apps/apigateway/README.md +11 -0
- apps/apigateway/src/apigateway.controller.spec.ts +22 -0
- apps/apigateway/src/apigateway.controller.ts +19 -0
- apps/apigateway/src/apigateway.module.ts +62 -0
- apps/apigateway/src/common/circuitbraker/circuitbraker.module.ts +20 -0
- apps/apigateway/src/common/circuitbraker/circuitbraker.service.spec.ts +18 -0
- apps/apigateway/src/common/circuitbraker/circuitbraker.service.ts +93 -0
- apps/apigateway/src/main.ts +58 -0
- apps/apigateway/src/middlewares/logger/logger.middleware.spec.ts +10 -0
- apps/apigateway/src/middlewares/logger/logger.middleware.ts +15 -0
- apps/apigateway/src/modules/azure/azure.controller.spec.ts +18 -0
- apps/apigateway/src/modules/azure/azure.controller.ts +27 -0
- apps/apigateway/src/modules/azure/azure.module.ts +33 -0
- apps/apigateway/src/modules/northwind/northwind.controller.spec.ts +18 -0
- apps/apigateway/src/modules/northwind/northwind.controller.ts +78 -0
- apps/apigateway/src/modules/northwind/northwind.module.ts +31 -0
- apps/apigateway/src/modules/northwind/northwind.service.spec.ts +18 -0
- apps/apigateway/src/modules/northwind/northwind.service.ts +79 -0
- apps/apigateway/src/modules/weather/weather.controller.spec.ts +18 -0
- apps/apigateway/src/modules/weather/weather.controller.ts +22 -0
- apps/apigateway/src/modules/weather/weather.module.ts +37 -0
- apps/apigateway/src/modules/weather/weather.service.spec.ts +18 -0
- apps/apigateway/src/modules/weather/weather.service.ts +61 -0
- apps/apigateway/src/service-map.json +7 -0
- apps/apigateway/test/app.e2e-spec.ts +24 -0
- apps/apigateway/test/jest-e2e.json +9 -0
- apps/apigateway/tsconfig.app.json +10 -0
- apps/azureapi/src/azureapi.controller.spec.ts +22 -0
- apps/azureapi/src/azureapi.controller.ts +16 -0
- apps/azureapi/src/azureapi.module.ts +11 -0
- apps/azureapi/src/azureapi.service.ts +25 -0
- apps/azureapi/src/main.ts +49 -0
- apps/azureapi/test/app.e2e-spec.ts +24 -0
- apps/azureapi/test/jest-e2e.json +9 -0
- apps/azureapi/tsconfig.app.json +9 -0
- apps/northwindapi/src/config/typeorm.config.ts +81 -0
- apps/northwindapi/src/database/migrations/1745487689382-migration.ts +91 -0
- apps/northwindapi/src/database/migrations/1748636404589-migration.ts +14 -0
- apps/northwindapi/src/database/seed/index.ts +36 -0
- apps/northwindapi/src/database/seed/seed-categories.ts +30 -0
- apps/northwindapi/src/database/seed/seed-customers.ts +148 -0
- apps/northwindapi/src/database/seed/seed-employees.ts +173 -0
- apps/northwindapi/src/database/seed/seed-order-details.ts +49 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,9 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
apps/oauthapp/src/public/images/OpenSource[[:space:]]Website[[:space:]]Load[[:space:]]After[[:space:]]dB[[:space:]]Restoration.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
apps/oauthapp/src/public/images/OpenSource[[:space:]]dB[[:space:]]Restoration[[:space:]]Proof.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
apps/oauthapp/src/public/images/apim.png filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
apps/oauthapp/src/public/images/apimpolicy.png filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
apps/oauthapp/src/public/images/botecosystem.png filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
apps/oauthapp/src/public/images/redisusages.png filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# compiled output
|
| 2 |
+
/dist
|
| 3 |
+
/node_modules
|
| 4 |
+
/build
|
| 5 |
+
|
| 6 |
+
# Logs
|
| 7 |
+
logs
|
| 8 |
+
*.log
|
| 9 |
+
npm-debug.log*
|
| 10 |
+
pnpm-debug.log*
|
| 11 |
+
yarn-debug.log*
|
| 12 |
+
yarn-error.log*
|
| 13 |
+
lerna-debug.log*
|
| 14 |
+
|
| 15 |
+
# OS
|
| 16 |
+
.DS_Store
|
| 17 |
+
|
| 18 |
+
# Tests
|
| 19 |
+
/coverage
|
| 20 |
+
/.nyc_output
|
| 21 |
+
|
| 22 |
+
# IDEs and editors
|
| 23 |
+
/.idea
|
| 24 |
+
.project
|
| 25 |
+
.classpath
|
| 26 |
+
.c9/
|
| 27 |
+
*.launch
|
| 28 |
+
.settings/
|
| 29 |
+
*.sublime-workspace
|
| 30 |
+
|
| 31 |
+
# IDE - VSCode
|
| 32 |
+
.vscode/*
|
| 33 |
+
!.vscode/settings.json
|
| 34 |
+
!.vscode/tasks.json
|
| 35 |
+
!.vscode/launch.json
|
| 36 |
+
!.vscode/extensions.json
|
| 37 |
+
|
| 38 |
+
# dotenv environment variable files
|
| 39 |
+
.env
|
| 40 |
+
.env.development.local
|
| 41 |
+
.env.test.local
|
| 42 |
+
.env.production.local
|
| 43 |
+
.env.local
|
| 44 |
+
|
| 45 |
+
# temp directory
|
| 46 |
+
.temp
|
| 47 |
+
.tmp
|
| 48 |
+
|
| 49 |
+
# Runtime data
|
| 50 |
+
pids
|
| 51 |
+
*.pid
|
| 52 |
+
*.seed
|
| 53 |
+
*.pid.lock
|
| 54 |
+
|
| 55 |
+
# Diagnostic reports (https://nodejs.org/api/report.html)
|
| 56 |
+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
.npmrc
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@bibhu2020:registry=https://npm.pkg.github.com
|
| 2 |
+
//npm.pkg.github.com/:_authToken=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
|
.prettierrc
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"singleQuote": true,
|
| 3 |
+
"trailingComma": "all"
|
| 4 |
+
}
|
Dockerfile
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use an official Node.js runtime as a parent image
|
| 2 |
+
FROM node:20-alpine AS builder
|
| 3 |
+
|
| 4 |
+
# Set working directory
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Install dependencies
|
| 8 |
+
COPY package*.json ./
|
| 9 |
+
RUN npm install
|
| 10 |
+
|
| 11 |
+
# Copy the rest of the app's source code
|
| 12 |
+
COPY . .
|
| 13 |
+
|
| 14 |
+
# Build the NestJS app
|
| 15 |
+
RUN npm run build:apigateway
|
| 16 |
+
|
| 17 |
+
RUN mkdir -p dist/libs/proto && cp -r libs/proto dist/libs
|
| 18 |
+
|
| 19 |
+
# Start the application
|
| 20 |
+
CMD ["node", "dist/apps/apigateway/main.js"]
|
| 21 |
+
|
| 22 |
+
# Expose app port
|
| 23 |
+
EXPOSE 8080
|
ENV_VARIABLES
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
DATABASE_URL=""
|
| 2 |
+
# uncomment next line if you use Prisma <5.10
|
| 3 |
+
# DATABASE_URL_UNPOOLED="postgresql://neondb_owner:npg_vkaFtG8d3XPJ@ep-fancy-hill-a8tm0pqc.eastus2.azure.neon.tech/neondb?sslmode=require"
|
| 4 |
+
AUTH_REDIRCET_URL=http://localhost:8080/auth/redirect
|
| 5 |
+
AZURE_CLIENT_ID=908e42d2-fe86-41b7-8220-6bd41930b3b4
|
| 6 |
+
AZURE_CLIENT_SECRET=
|
| 7 |
+
AZURE_TENANT_ID=95595fde-7f04-4030-ace9-d08cf1e81bdc
|
| 8 |
+
MANAGED_IDENTITY_CLIENT_ID=ddd
|
| 9 |
+
NODE_ENV=development
|
| 10 |
+
# Weather Service 3rd Party API (optional if you do not want to test 3rd party API: https://api.openweathermap.org)
|
| 11 |
+
WEATHER_SERVICE_API_KEY=
|
README.md
CHANGED
|
@@ -1,10 +1,14 @@
|
|
| 1 |
---
|
| 2 |
-
title: Northwind
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Northwind API Gateway
|
| 3 |
+
emoji: 🐳
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: docker
|
| 7 |
+
app_file: app.py
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# 🐳 Northwind API Gateway
|
| 12 |
+
|
| 13 |
+
Dockerized NestJS API Gateway for the Northwind dataset, running on Node.js 20 and exposing port 8080.
|
| 14 |
+
EOF
|
apps/apigateway/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## Required Packages
|
| 2 |
+
```bash
|
| 3 |
+
npm install @nestjs/axios @nestjs/config @nestjs/throttler opossum
|
| 4 |
+
|
| 5 |
+
```
|
| 6 |
+
|
| 7 |
+
## Add logging middleware to the monorepo
|
| 8 |
+
this middleware intercept all requests hitting the microservices, and log them.
|
| 9 |
+
```bash
|
| 10 |
+
nest g middleware logger --project apigateway
|
| 11 |
+
```
|
apps/apigateway/src/apigateway.controller.spec.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Test, TestingModule } from '@nestjs/testing';
|
| 2 |
+
import { ApigatewayController } from './apigateway.controller';
|
| 3 |
+
import { ApigatewayService } from './apigateway.service';
|
| 4 |
+
|
| 5 |
+
describe('ApigatewayController', () => {
|
| 6 |
+
let apigatewayController: ApigatewayController;
|
| 7 |
+
|
| 8 |
+
beforeEach(async () => {
|
| 9 |
+
const app: TestingModule = await Test.createTestingModule({
|
| 10 |
+
controllers: [ApigatewayController],
|
| 11 |
+
providers: [ApigatewayService],
|
| 12 |
+
}).compile();
|
| 13 |
+
|
| 14 |
+
apigatewayController = app.get<ApigatewayController>(ApigatewayController);
|
| 15 |
+
});
|
| 16 |
+
|
| 17 |
+
describe('root', () => {
|
| 18 |
+
it('should return "Hello World!"', () => {
|
| 19 |
+
expect(apigatewayController.getHello()).toBe('Hello World!');
|
| 20 |
+
});
|
| 21 |
+
});
|
| 22 |
+
});
|
apps/apigateway/src/apigateway.controller.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Controller, Req, Res, All, Get, Post } from '@nestjs/common';
|
| 2 |
+
import { Request, Response } from 'express';
|
| 3 |
+
|
| 4 |
+
@Controller(['/', '/healthz'])
|
| 5 |
+
export class ApigatewayController {
|
| 6 |
+
|
| 7 |
+
@Get()
|
| 8 |
+
async healthz(@Req() req: Request, @Res() res: Response) {
|
| 9 |
+
console.log('Request received:', req.method, req.url);
|
| 10 |
+
res.send('🚀 ApiGateway is running...............');
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
@All()
|
| 14 |
+
async test(@Req() req: Request, @Res() res: Response) {
|
| 15 |
+
console.log('POST Request received:', req.method, req.url);
|
| 16 |
+
res.send('🚀 ApiGateway is running...............');
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
}
|
apps/apigateway/src/apigateway.module.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
|
| 2 |
+
import { ConfigModule } from '@nestjs/config';
|
| 3 |
+
import { ThrottlerModule } from '@nestjs/throttler';
|
| 4 |
+
import { HttpModule } from '@nestjs/axios';
|
| 5 |
+
|
| 6 |
+
import { ApigatewayController } from './apigateway.controller';
|
| 7 |
+
import { ApploggerModule } from '@bpm/common';
|
| 8 |
+
import { LoggerMiddleware } from './middlewares/logger/logger.middleware';
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
import { NorthwindModule } from './modules/northwind/northwind.module';
|
| 12 |
+
import { AzureModule } from './modules/azure/azure.module';
|
| 13 |
+
import { WeatherModule } from './modules/weather/weather.module';
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@Module({
|
| 17 |
+
imports: [
|
| 18 |
+
// ✅ Correct usage of ThrottlerModule (Rate Limiting)
|
| 19 |
+
// The limit is the maximum number of requests allowed within the ttl
|
| 20 |
+
// This configuration allows for 3 requests per 1 second (short limit)
|
| 21 |
+
// and 100 requests per 1 minute (long limit)
|
| 22 |
+
ThrottlerModule.forRoot([
|
| 23 |
+
{
|
| 24 |
+
name: 'short',
|
| 25 |
+
ttl: 1000, // 3 requests per 1 seconds (short limit)
|
| 26 |
+
limit: 1,
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
name: 'long',
|
| 30 |
+
ttl: 60000, // 100 requests per 1 minute (long limit)
|
| 31 |
+
limit: 100,
|
| 32 |
+
}
|
| 33 |
+
]),
|
| 34 |
+
|
| 35 |
+
//ConfigModule should be imported with isGlobal: true
|
| 36 |
+
// This makes the configuration available globally in the application
|
| 37 |
+
ConfigModule.forRoot({ isGlobal: true }),
|
| 38 |
+
|
| 39 |
+
// ✅ HttpModule should not be passed directly like a class
|
| 40 |
+
// It provides a simple and easy-to-use interface for making HTTP requests
|
| 41 |
+
HttpModule,
|
| 42 |
+
|
| 43 |
+
ApploggerModule,
|
| 44 |
+
|
| 45 |
+
NorthwindModule,
|
| 46 |
+
|
| 47 |
+
AzureModule,
|
| 48 |
+
|
| 49 |
+
WeatherModule,
|
| 50 |
+
|
| 51 |
+
],
|
| 52 |
+
controllers: [ApigatewayController],
|
| 53 |
+
providers: [LoggerMiddleware],
|
| 54 |
+
})
|
| 55 |
+
export class ApigatewayModule implements NestModule {
|
| 56 |
+
// No need to bind manually if LoggerMiddleware is injectable
|
| 57 |
+
configure(consumer: MiddlewareConsumer) {
|
| 58 |
+
consumer
|
| 59 |
+
.apply(LoggerMiddleware) // Apply LoggerMiddleware directly
|
| 60 |
+
.forRoutes('*'); // Apply to all routes
|
| 61 |
+
}
|
| 62 |
+
}
|
apps/apigateway/src/common/circuitbraker/circuitbraker.module.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Module } from '@nestjs/common';
|
| 2 |
+
import { CircuitbrakerService } from './circuitbraker.service';
|
| 3 |
+
import { ApploggerService } from '@bpm/common';
|
| 4 |
+
|
| 5 |
+
@Module({
|
| 6 |
+
providers: [{
|
| 7 |
+
provide: CircuitbrakerService,
|
| 8 |
+
useFactory: (logger: ApploggerService) => {
|
| 9 |
+
return new CircuitbrakerService({
|
| 10 |
+
failureThreshold: 3, // Fail after 3 consecutive failures
|
| 11 |
+
successThreshold: 2, // Recover after 2 consecutive successes
|
| 12 |
+
timeout: 5000, // Timeout after 5 seconds
|
| 13 |
+
serviceName: 'apigateway', // The name of the service being protected by the circuit breaker
|
| 14 |
+
}, logger);
|
| 15 |
+
},
|
| 16 |
+
inject: [ApploggerService],
|
| 17 |
+
}],
|
| 18 |
+
exports: [CircuitbrakerService], // <-- must export it
|
| 19 |
+
})
|
| 20 |
+
export class CircuitbrakerModule {}
|
apps/apigateway/src/common/circuitbraker/circuitbraker.service.spec.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Test, TestingModule } from '@nestjs/testing';
|
| 2 |
+
import { CircuitbrakerService } from './circuitbraker.service';
|
| 3 |
+
|
| 4 |
+
describe('CircuitbrakerService', () => {
|
| 5 |
+
let service: CircuitbrakerService;
|
| 6 |
+
|
| 7 |
+
beforeEach(async () => {
|
| 8 |
+
const module: TestingModule = await Test.createTestingModule({
|
| 9 |
+
providers: [CircuitbrakerService],
|
| 10 |
+
}).compile();
|
| 11 |
+
|
| 12 |
+
service = module.get<CircuitbrakerService>(CircuitbrakerService);
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
it('should be defined', () => {
|
| 16 |
+
expect(service).toBeDefined();
|
| 17 |
+
});
|
| 18 |
+
});
|
apps/apigateway/src/common/circuitbraker/circuitbraker.service.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ApploggerService } from '@bpm/common';
|
| 2 |
+
import { Injectable } from '@nestjs/common';
|
| 3 |
+
|
| 4 |
+
export type CircuitBreakerState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
|
| 5 |
+
|
| 6 |
+
interface CircuitBreakerOptions {
|
| 7 |
+
failureThreshold: number;
|
| 8 |
+
successThreshold: number;
|
| 9 |
+
timeout: number; // in ms
|
| 10 |
+
serviceName: string; // Service name to track which service is failing
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
@Injectable()
|
| 14 |
+
export class CircuitbrakerService {
|
| 15 |
+
private state: CircuitBreakerState = 'CLOSED';
|
| 16 |
+
private failureCount = 0;
|
| 17 |
+
private successCount = 0;
|
| 18 |
+
private nextAttempt = Date.now();
|
| 19 |
+
|
| 20 |
+
constructor(private options: CircuitBreakerOptions,
|
| 21 |
+
private readonly logger: ApploggerService
|
| 22 |
+
) {}
|
| 23 |
+
|
| 24 |
+
async call<T>(fn: () => Promise<T>): Promise<T> {
|
| 25 |
+
if (this.state === 'OPEN') {
|
| 26 |
+
if (Date.now() > this.nextAttempt) {
|
| 27 |
+
this.state = 'HALF_OPEN';
|
| 28 |
+
//this.logger.log(`${this.options.serviceName} - Circuit is HALF_OPEN. Attempting recovery.`, CircuitbrakerService.name);
|
| 29 |
+
} else {
|
| 30 |
+
//this.logger.log(`${this.options.serviceName} - Circuit is OPEN. Request blocked.`, CircuitbrakerService.name);
|
| 31 |
+
throw new Error(`${this.options.serviceName} - Circuit is OPEN`);
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
try {
|
| 36 |
+
const result = await fn();
|
| 37 |
+
this.onSuccess();
|
| 38 |
+
return result;
|
| 39 |
+
} catch (err) {
|
| 40 |
+
this.onFailure(err);
|
| 41 |
+
throw err;
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
private onSuccess() {
|
| 46 |
+
if (this.state === 'HALF_OPEN') {
|
| 47 |
+
this.successCount++;
|
| 48 |
+
//this.logger.log(`${this.options.serviceName} - Success count: ${this.successCount}`, CircuitbrakerService.name);
|
| 49 |
+
if (this.successCount >= this.options.successThreshold) {
|
| 50 |
+
this.reset();
|
| 51 |
+
//this.logger.log(`${this.options.serviceName} - Circuit is now CLOSED`, CircuitbrakerService.name);
|
| 52 |
+
}
|
| 53 |
+
} else {
|
| 54 |
+
this.reset();
|
| 55 |
+
//this.logger.log(`${this.options.serviceName} - Circuit is CLOSED after success`, CircuitbrakerService.name);
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
private onFailure(err: any) {
|
| 60 |
+
const status = err?.response?.status || err?.status || 500;
|
| 61 |
+
|
| 62 |
+
if (status <= 400) {
|
| 63 |
+
//this.logger.log(`${this.options.serviceName} - Ignoring non-error status: ${status}`, CircuitbrakerService.name);
|
| 64 |
+
return; // Don't count it as failure
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
this.failureCount++;
|
| 68 |
+
//this.logger.log(`${this.options.serviceName} - Failure count: ${this.failureCount}`, CircuitbrakerService.name);
|
| 69 |
+
this.logger.error(`${this.options.serviceName} - Failure: ${err.message || 'Unknown error'}`, CircuitbrakerService.name);
|
| 70 |
+
|
| 71 |
+
if (this.failureCount >= this.options.failureThreshold) {
|
| 72 |
+
this.trip();
|
| 73 |
+
//this.logger.log(`${this.options.serviceName} - Circuit is OPEN due to repeated failures`, CircuitbrakerService.name);
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
private reset() {
|
| 78 |
+
this.failureCount = 0;
|
| 79 |
+
this.successCount = 0;
|
| 80 |
+
this.state = 'CLOSED';
|
| 81 |
+
//this.logger.log(`${this.options.serviceName} - Circuit is RESET to CLOSED`, CircuitbrakerService.name);
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
private trip() {
|
| 85 |
+
this.state = 'OPEN';
|
| 86 |
+
this.nextAttempt = Date.now() + this.options.timeout;
|
| 87 |
+
//this.logger.log(`${this.options.serviceName} - Circuit is TRIPPED to OPEN. Next attempt will be after ${this.options.timeout}ms.`, CircuitbrakerService.name);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
public getState() {
|
| 91 |
+
return this.state;
|
| 92 |
+
}
|
| 93 |
+
}
|
apps/apigateway/src/main.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NestFactory } from '@nestjs/core';
|
| 2 |
+
import { ApigatewayModule } from './apigateway.module';
|
| 3 |
+
import { ApploggerService } from '@bpm/common';
|
| 4 |
+
|
| 5 |
+
async function bootstrap() {
|
| 6 |
+
const context = 'apigateway';
|
| 7 |
+
const logger = new ApploggerService(context);
|
| 8 |
+
|
| 9 |
+
logger.log('🛠️ Starting up the NestJS Application...\n', context);
|
| 10 |
+
logger.log('📦 Loading modules...\n', context);
|
| 11 |
+
|
| 12 |
+
const app = await NestFactory.create(ApigatewayModule);
|
| 13 |
+
|
| 14 |
+
app.enableShutdownHooks(); // 👈 Add this
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
app.enableCors({
|
| 18 |
+
origin: [/http:\/\/localhost:\d+$/, 'https://srepoc-northwind-vuejs-ctbmh6bufhgufqea.b01.azurefd.net'], // Allow all requests from localhost on any port
|
| 19 |
+
credentials: true, // Enable credentials if needed (cookies, auth headers, etc.)
|
| 20 |
+
});
|
| 21 |
+
|
| 22 |
+
app.setGlobalPrefix('api'); // 👈 This sets /api as the prefix
|
| 23 |
+
|
| 24 |
+
const port = process.env.PORT || 3001;
|
| 25 |
+
|
| 26 |
+
try {
|
| 27 |
+
await app.listen(port, () => {
|
| 28 |
+
logger.log('✅ App Modules initialized successfully\n', context);
|
| 29 |
+
logger.log('🌐 Enabling global configurations...\n', context);
|
| 30 |
+
|
| 31 |
+
logger.log(
|
| 32 |
+
`🚀 Application is running at: http://localhost:${port}\n`,
|
| 33 |
+
context,
|
| 34 |
+
);
|
| 35 |
+
logger.log('📡 Ready to accept incoming requests!\n', context);
|
| 36 |
+
logger.log('🧠 Powered by NestJS ❤️\n', context);
|
| 37 |
+
});
|
| 38 |
+
} catch (err) {
|
| 39 |
+
logger.error('❌ Failed to start application\n', err.stack);
|
| 40 |
+
if (app) {
|
| 41 |
+
await app.close();
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// OS signal listeners (optional)
|
| 46 |
+
process.on('SIGINT', async () => {
|
| 47 |
+
logger.warn('🛑 SIGINT received. Gracefully shutting down...', context);
|
| 48 |
+
await app.close();
|
| 49 |
+
process.exit(1);
|
| 50 |
+
});
|
| 51 |
+
|
| 52 |
+
process.on('SIGTERM', async () => {
|
| 53 |
+
logger.warn('🛑 SIGTERM received. Gracefully shutting down...', context);
|
| 54 |
+
await app.close();
|
| 55 |
+
process.exit(1);
|
| 56 |
+
});
|
| 57 |
+
}
|
| 58 |
+
bootstrap();
|
apps/apigateway/src/middlewares/logger/logger.middleware.spec.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { LoggerMiddleware } from './logger.middleware';
|
| 2 |
+
|
| 3 |
+
import { ApploggerService } from '@bpm/common/app-logger/applogger.service';
|
| 4 |
+
|
| 5 |
+
describe('LoggerMiddleware', () => {
|
| 6 |
+
it('should be defined', () => {
|
| 7 |
+
const mockLogger = {} as ApploggerService;
|
| 8 |
+
expect(new LoggerMiddleware(mockLogger)).toBeDefined();
|
| 9 |
+
});
|
| 10 |
+
});
|
apps/apigateway/src/middlewares/logger/logger.middleware.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ApploggerService } from '@bpm/common';
|
| 2 |
+
import { Injectable, NestMiddleware } from '@nestjs/common';
|
| 3 |
+
|
| 4 |
+
@Injectable()
|
| 5 |
+
export class LoggerMiddleware implements NestMiddleware {
|
| 6 |
+
constructor(private readonly logger: ApploggerService) {}
|
| 7 |
+
|
| 8 |
+
use(req: any, res: any, next: () => void) {
|
| 9 |
+
// Make sure to log relevant request details
|
| 10 |
+
this.logger.log(`[${req.method}] ${req.originalUrl}`, "apigateway"); // Log method and URL
|
| 11 |
+
// Log headers or any other information if needed
|
| 12 |
+
//this.logger.log(`Headers: ${JSON.stringify(req.headers)}`, "apigateway");
|
| 13 |
+
next();
|
| 14 |
+
}
|
| 15 |
+
}
|
apps/apigateway/src/modules/azure/azure.controller.spec.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Test, TestingModule } from '@nestjs/testing';
|
| 2 |
+
import { AzureController } from './azure.controller';
|
| 3 |
+
|
| 4 |
+
describe('AzureController', () => {
|
| 5 |
+
let controller: AzureController;
|
| 6 |
+
|
| 7 |
+
beforeEach(async () => {
|
| 8 |
+
const module: TestingModule = await Test.createTestingModule({
|
| 9 |
+
controllers: [AzureController],
|
| 10 |
+
}).compile();
|
| 11 |
+
|
| 12 |
+
controller = module.get<AzureController>(AzureController);
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
it('should be defined', () => {
|
| 16 |
+
expect(controller).toBeDefined();
|
| 17 |
+
});
|
| 18 |
+
});
|
apps/apigateway/src/modules/azure/azure.controller.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Controller, Get, Inject, OnModuleInit, Query } from '@nestjs/common';
|
| 2 |
+
import { ClientGrpc } from '@nestjs/microservices';
|
| 3 |
+
import { Observable } from 'rxjs';
|
| 4 |
+
|
| 5 |
+
interface AzureApiService {
|
| 6 |
+
readSecret(data: { kvName: string; secretName: string }): Observable<{
|
| 7 |
+
kvName: string;
|
| 8 |
+
secretName: string;
|
| 9 |
+
secretValue: string;
|
| 10 |
+
}>;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
@Controller('azure')
|
| 14 |
+
export class AzureController implements OnModuleInit {
|
| 15 |
+
private grpcService: AzureApiService;
|
| 16 |
+
|
| 17 |
+
constructor(@Inject('AZURE_API_PACKAGE') private readonly client: ClientGrpc) {}
|
| 18 |
+
|
| 19 |
+
onModuleInit() {
|
| 20 |
+
this.grpcService = this.client.getService<AzureApiService>('AzureApiService');
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
@Get('secret')
|
| 24 |
+
async getSecret(@Query('kv') kv: string, @Query('secret') secret: string) {
|
| 25 |
+
return this.grpcService.readSecret({ kvName: kv, secretName: secret });
|
| 26 |
+
}
|
| 27 |
+
}
|
apps/apigateway/src/modules/azure/azure.module.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Module } from '@nestjs/common';
|
| 2 |
+
import { ClientsModule, Transport } from '@nestjs/microservices';
|
| 3 |
+
import { join } from 'path';
|
| 4 |
+
import { AzureController } from './azure.controller';
|
| 5 |
+
|
| 6 |
+
const isProd = process.env.NODE_ENV === 'production';
|
| 7 |
+
const protoPath = isProd
|
| 8 |
+
? '../../../libs/proto/azureapi.proto'
|
| 9 |
+
: '../../../../../libs/proto/azureapi.proto';
|
| 10 |
+
|
| 11 |
+
//console.log('protoPath', join(__dirname, protoPath));
|
| 12 |
+
const port = process.env.PORT || 3021;
|
| 13 |
+
const apiURL = isProd
|
| 14 |
+
? process.env.PROD_AZUREAPI_URL || 'weatherapi-nestjs-service.riskiq.svc.cluster.local'
|
| 15 |
+
: 'localhost:' + port;
|
| 16 |
+
|
| 17 |
+
@Module({
|
| 18 |
+
imports: [
|
| 19 |
+
ClientsModule.register([
|
| 20 |
+
{
|
| 21 |
+
name: 'AZURE_API_PACKAGE',
|
| 22 |
+
transport: Transport.GRPC,
|
| 23 |
+
options: {
|
| 24 |
+
package: 'azureapi',
|
| 25 |
+
protoPath: join(__dirname, protoPath), // 👈 Adjust if needed
|
| 26 |
+
url: apiURL, // 👈 Adjust to your gRPC server host/port
|
| 27 |
+
},
|
| 28 |
+
},
|
| 29 |
+
]),
|
| 30 |
+
],
|
| 31 |
+
controllers: [AzureController]
|
| 32 |
+
})
|
| 33 |
+
export class AzureModule {}
|
apps/apigateway/src/modules/northwind/northwind.controller.spec.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Test, TestingModule } from '@nestjs/testing';
|
| 2 |
+
import { NorthwindController } from './northwind.controller';
|
| 3 |
+
|
| 4 |
+
describe('NorthwindapiController', () => {
|
| 5 |
+
let controller: NorthwindController;
|
| 6 |
+
|
| 7 |
+
beforeEach(async () => {
|
| 8 |
+
const module: TestingModule = await Test.createTestingModule({
|
| 9 |
+
controllers: [NorthwindController],
|
| 10 |
+
}).compile();
|
| 11 |
+
|
| 12 |
+
controller = module.get<NorthwindController>(NorthwindController);
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
it('should be defined', () => {
|
| 16 |
+
expect(controller).toBeDefined();
|
| 17 |
+
});
|
| 18 |
+
});
|
apps/apigateway/src/modules/northwind/northwind.controller.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Controller, Req, Res, All, Get } from '@nestjs/common';
|
| 2 |
+
import { NorthwindService } from './northwind.service';
|
| 3 |
+
import { Request, Response } from 'express';
|
| 4 |
+
|
| 5 |
+
@Controller('northwind')
|
| 6 |
+
export class NorthwindController {
|
| 7 |
+
constructor(private readonly gateway: NorthwindService) {}
|
| 8 |
+
|
| 9 |
+
@Get('/')
|
| 10 |
+
async get(@Req() req: Request, @Res() res: Response) {
|
| 11 |
+
//console.log('Request received:', req.method, req.url);
|
| 12 |
+
res.send('🚀 You have reached northwind service...............');
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
@Get(['/swagger', '/swagger/*'])
|
| 16 |
+
async swagger(@Req() req: Request, @Res() res: Response) {
|
| 17 |
+
const baseUrl = "/api/northwind";
|
| 18 |
+
const strippedPath = req.url.toLocaleLowerCase().slice(baseUrl.length);
|
| 19 |
+
const isProd = process.env.NODE_ENV === 'production';
|
| 20 |
+
|
| 21 |
+
const targetBaseUrl = isProd
|
| 22 |
+
? 'http://northwindapi-nestjs-service.riskiq.svc.cluster.local'
|
| 23 |
+
: 'http://localhost:3002';
|
| 24 |
+
|
| 25 |
+
console.log(strippedPath);
|
| 26 |
+
|
| 27 |
+
const result = await this.gateway.forwardRequest(
|
| 28 |
+
targetBaseUrl,
|
| 29 |
+
req.method as any,
|
| 30 |
+
strippedPath,
|
| 31 |
+
req.headers,
|
| 32 |
+
undefined
|
| 33 |
+
);
|
| 34 |
+
|
| 35 |
+
// Attempt to parse and transform JSON paths
|
| 36 |
+
if (typeof result === 'object' && result?.paths) {
|
| 37 |
+
const transformed = {
|
| 38 |
+
...result,
|
| 39 |
+
paths: {},
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
for (const [path, value] of Object.entries(result.paths)) {
|
| 43 |
+
const newPath = path.replace(/^\/api/, baseUrl);
|
| 44 |
+
transformed.paths[newPath] = value;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
return res.json(transformed);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
// Fallback to regular response
|
| 51 |
+
return res.send(result);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@All('*')
|
| 57 |
+
async allRequests(@Req() req: Request, @Res() res: Response) {
|
| 58 |
+
const baseUrl = "/api/northwind";
|
| 59 |
+
const strippedPath = req.url.toLocaleLowerCase().slice(baseUrl.length);
|
| 60 |
+
//console.log('Request received:', req.method, strippedPath);
|
| 61 |
+
const isProd = process.env.NODE_ENV === 'production';
|
| 62 |
+
let targetBaseUrl = isProd
|
| 63 |
+
? 'http://northwindapi-nestjs-service.riskiq.svc.cluster.local/api'
|
| 64 |
+
: 'http://localhost:3002/api';
|
| 65 |
+
|
| 66 |
+
const hasBody = !['GET', 'HEAD'].includes(req.method.toUpperCase());
|
| 67 |
+
|
| 68 |
+
const result = await this.gateway.forwardRequest(
|
| 69 |
+
targetBaseUrl,
|
| 70 |
+
req.method as any,
|
| 71 |
+
strippedPath,
|
| 72 |
+
req.headers,
|
| 73 |
+
hasBody ? req.body : undefined
|
| 74 |
+
);
|
| 75 |
+
|
| 76 |
+
res.send(result);
|
| 77 |
+
}
|
| 78 |
+
}
|
apps/apigateway/src/modules/northwind/northwind.module.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Module } from '@nestjs/common';
|
| 2 |
+
import { NorthwindController } from './northwind.controller';
|
| 3 |
+
import { NorthwindService } from './northwind.service';
|
| 4 |
+
import { CircuitbrakerModule } from '../../common/circuitbraker/circuitbraker.module';
|
| 5 |
+
import { CircuitbrakerService } from '../../common/circuitbraker/circuitbraker.service';
|
| 6 |
+
import { HttpModule } from '@nestjs/axios';
|
| 7 |
+
import { ApploggerService } from '@bpm/common';
|
| 8 |
+
|
| 9 |
+
@Module({
|
| 10 |
+
imports: [
|
| 11 |
+
HttpModule, // ✅ Import this
|
| 12 |
+
CircuitbrakerModule // ✅ Import CircuitbreakerService from its module
|
| 13 |
+
],
|
| 14 |
+
controllers: [NorthwindController],
|
| 15 |
+
providers: [NorthwindService,
|
| 16 |
+
{
|
| 17 |
+
provide: CircuitbrakerService,
|
| 18 |
+
useFactory: (logger: ApploggerService) => {
|
| 19 |
+
return new CircuitbrakerService({
|
| 20 |
+
failureThreshold: 3, // Fail after 3 consecutive failures
|
| 21 |
+
successThreshold: 2, // Recover after 2 consecutive successes
|
| 22 |
+
timeout: 5000, // Timeout after 5 seconds
|
| 23 |
+
serviceName: 'apigateway', // The name of the service being protected by the circuit breaker
|
| 24 |
+
}, logger);
|
| 25 |
+
},
|
| 26 |
+
inject: [ApploggerService],
|
| 27 |
+
},
|
| 28 |
+
],
|
| 29 |
+
exports: [NorthwindService],
|
| 30 |
+
})
|
| 31 |
+
export class NorthwindModule {}
|
apps/apigateway/src/modules/northwind/northwind.service.spec.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Test, TestingModule } from '@nestjs/testing';
|
| 2 |
+
import { NorthwindService } from './northwind.service';
|
| 3 |
+
|
| 4 |
+
describe('NorthwindapiService', () => {
|
| 5 |
+
let service: NorthwindService;
|
| 6 |
+
|
| 7 |
+
beforeEach(async () => {
|
| 8 |
+
const module: TestingModule = await Test.createTestingModule({
|
| 9 |
+
providers: [NorthwindService],
|
| 10 |
+
}).compile();
|
| 11 |
+
|
| 12 |
+
service = module.get<NorthwindService>(NorthwindService);
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
it('should be defined', () => {
|
| 16 |
+
expect(service).toBeDefined();
|
| 17 |
+
});
|
| 18 |
+
});
|
apps/apigateway/src/modules/northwind/northwind.service.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable, InternalServerErrorException } from '@nestjs/common';
|
| 2 |
+
import { HttpService } from '@nestjs/axios';
|
| 3 |
+
//import { lastValueFrom } from 'rxjs';
|
| 4 |
+
import { AxiosRequestConfig } from 'axios';
|
| 5 |
+
import { CircuitbrakerService } from '../../common/circuitbraker/circuitbraker.service';
|
| 6 |
+
import { ApploggerService } from '@bpm/common';
|
| 7 |
+
|
| 8 |
+
@Injectable()
|
| 9 |
+
export class NorthwindService {
|
| 10 |
+
constructor(
|
| 11 |
+
private readonly http: HttpService,
|
| 12 |
+
private readonly circuitBreaker: CircuitbrakerService,
|
| 13 |
+
private readonly logger: ApploggerService
|
| 14 |
+
) {}
|
| 15 |
+
|
| 16 |
+
async forwardRequest(
|
| 17 |
+
baseUrl: string,
|
| 18 |
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD',
|
| 19 |
+
path: string,
|
| 20 |
+
headers: any,
|
| 21 |
+
body: any,
|
| 22 |
+
) {
|
| 23 |
+
const url = `${baseUrl.replace(/\/$/, '')}/${path.replace(/^\//, '')}`;
|
| 24 |
+
this.logger.log(`Forwarding to ${url} using ${method}`, NorthwindService.name);
|
| 25 |
+
|
| 26 |
+
// Prepare default config
|
| 27 |
+
const config: AxiosRequestConfig = {
|
| 28 |
+
headers: {...headers },
|
| 29 |
+
url,
|
| 30 |
+
method,
|
| 31 |
+
validateStatus: (status) => status <= 500, // Avoid errors for 304, etc.
|
| 32 |
+
};
|
| 33 |
+
|
| 34 |
+
// Conditionally merge headers based on HTTP method
|
| 35 |
+
if (method === 'GET' || method === 'HEAD') {
|
| 36 |
+
// Create a copy of headers without 'content-type'
|
| 37 |
+
const { ['content-type']: _, ...headersWithoutContentType } = headers;
|
| 38 |
+
|
| 39 |
+
config.headers = {
|
| 40 |
+
...headersWithoutContentType,
|
| 41 |
+
accept: 'application/json',
|
| 42 |
+
'cache-control': 'no-cache',
|
| 43 |
+
// 'if-none-match': '', // explicitly force full response (disable ETag)
|
| 44 |
+
}; // Keep original headers for GET and HEAD
|
| 45 |
+
} else {
|
| 46 |
+
config.headers = {
|
| 47 |
+
accept: 'application/json',
|
| 48 |
+
'content-type': 'application/json', // Only for POST, PUT, DELETE, etc.
|
| 49 |
+
'cache-control': 'no-cache',
|
| 50 |
+
'if-none-match': '', // explicitly force full response (disable ETag)
|
| 51 |
+
};
|
| 52 |
+
config.data = body; // Add data only for methods other than GET/HEAD
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
//this.logger.log(config, NorthwindService.name);
|
| 57 |
+
|
| 58 |
+
try {
|
| 59 |
+
//this.logger.log('Sending request to service through circuit breaker', NorthwindService.name);
|
| 60 |
+
const response = await this.circuitBreaker.call(() =>
|
| 61 |
+
this.http.request(config).toPromise(), // toPromise() converts observable to promise
|
| 62 |
+
);
|
| 63 |
+
// Check if response is defined
|
| 64 |
+
if (!response) {
|
| 65 |
+
throw new InternalServerErrorException('No response received from service');
|
| 66 |
+
}
|
| 67 |
+
return response.data;
|
| 68 |
+
} catch (error) {
|
| 69 |
+
console.error(`Request failed:`, {
|
| 70 |
+
message: error?.message,
|
| 71 |
+
code: error?.code,
|
| 72 |
+
response: error?.response?.data,
|
| 73 |
+
status: error?.response?.status,
|
| 74 |
+
stack: error?.stack,
|
| 75 |
+
});
|
| 76 |
+
throw new InternalServerErrorException('Failed to reach service');
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
}
|
apps/apigateway/src/modules/weather/weather.controller.spec.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Test, TestingModule } from '@nestjs/testing';
|
| 2 |
+
import { WeatherController } from './weather.controller';
|
| 3 |
+
|
| 4 |
+
describe('WeatherController', () => {
|
| 5 |
+
let controller: WeatherController;
|
| 6 |
+
|
| 7 |
+
beforeEach(async () => {
|
| 8 |
+
const module: TestingModule = await Test.createTestingModule({
|
| 9 |
+
controllers: [WeatherController],
|
| 10 |
+
}).compile();
|
| 11 |
+
|
| 12 |
+
controller = module.get<WeatherController>(WeatherController);
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
it('should be defined', () => {
|
| 16 |
+
expect(controller).toBeDefined();
|
| 17 |
+
});
|
| 18 |
+
});
|
apps/apigateway/src/modules/weather/weather.controller.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// apps/apigateway/src/weather/weather.controller.ts
|
| 2 |
+
import { Controller, Get, Query } from '@nestjs/common';
|
| 3 |
+
import { WeatherService } from './weather.service';
|
| 4 |
+
|
| 5 |
+
@Controller('weather')
|
| 6 |
+
export class WeatherController {
|
| 7 |
+
constructor(private readonly weatherService: WeatherService) {}
|
| 8 |
+
|
| 9 |
+
@Get('current')
|
| 10 |
+
getCurrentWeather(@Query('city') city: string, @Query('country') country: string) {
|
| 11 |
+
return this.weatherService.getCurrentWeather(city, country);
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
@Get('forecast')
|
| 15 |
+
getForecast(
|
| 16 |
+
@Query('city') city: string,
|
| 17 |
+
@Query('country') country: string,
|
| 18 |
+
@Query('days') days: string,
|
| 19 |
+
) {
|
| 20 |
+
return this.weatherService.getForecast(city, country, parseInt(days));
|
| 21 |
+
}
|
| 22 |
+
}
|
apps/apigateway/src/modules/weather/weather.module.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Module } from '@nestjs/common';
|
| 2 |
+
import { ClientsModule, Transport } from '@nestjs/microservices';
|
| 3 |
+
import { join } from 'path';
|
| 4 |
+
import { WeatherController } from './weather.controller';
|
| 5 |
+
import { WeatherService } from './weather.service';
|
| 6 |
+
|
| 7 |
+
const isProd = process.env.NODE_ENV === 'production';
|
| 8 |
+
const protoPath = isProd
|
| 9 |
+
? '../../../libs/proto/weatherapi.proto'
|
| 10 |
+
: '../../../../../libs/proto/weatherapi.proto';
|
| 11 |
+
|
| 12 |
+
//console.log('protoPath', join(__dirname, protoPath));
|
| 13 |
+
const port = process.env.PORT || 3022;
|
| 14 |
+
const apiURL = isProd
|
| 15 |
+
? process.env.PROD_WEATHERAPI_URL || 'weatherapi-nestjs-service.riskiq.svc.cluster.local'
|
| 16 |
+
: 'localhost:' + port;
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@Module({
|
| 21 |
+
imports: [
|
| 22 |
+
ClientsModule.register([
|
| 23 |
+
{
|
| 24 |
+
name: 'WEATHER_API_PACKAGE',
|
| 25 |
+
transport: Transport.GRPC,
|
| 26 |
+
options: {
|
| 27 |
+
package: 'weatherapi',
|
| 28 |
+
protoPath: join(__dirname, protoPath), // 👈 Adjust if needed
|
| 29 |
+
url: apiURL, // 👈 Adjust to your gRPC server host/port
|
| 30 |
+
},
|
| 31 |
+
},
|
| 32 |
+
]),
|
| 33 |
+
],
|
| 34 |
+
controllers: [WeatherController],
|
| 35 |
+
providers: [WeatherService]
|
| 36 |
+
})
|
| 37 |
+
export class WeatherModule {}
|
apps/apigateway/src/modules/weather/weather.service.spec.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Test, TestingModule } from '@nestjs/testing';
|
| 2 |
+
import { WeatherService } from './weather.service';
|
| 3 |
+
|
| 4 |
+
describe('WeatherService', () => {
|
| 5 |
+
let service: WeatherService;
|
| 6 |
+
|
| 7 |
+
beforeEach(async () => {
|
| 8 |
+
const module: TestingModule = await Test.createTestingModule({
|
| 9 |
+
providers: [WeatherService],
|
| 10 |
+
}).compile();
|
| 11 |
+
|
| 12 |
+
service = module.get<WeatherService>(WeatherService);
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
it('should be defined', () => {
|
| 16 |
+
expect(service).toBeDefined();
|
| 17 |
+
});
|
| 18 |
+
});
|
apps/apigateway/src/modules/weather/weather.service.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// apps/apigateway/src/weather/weather.service.ts
|
| 2 |
+
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
| 3 |
+
import { ClientGrpc } from '@nestjs/microservices';
|
| 4 |
+
import { Observable, lastValueFrom } from 'rxjs';
|
| 5 |
+
import * as opossum from 'opossum';
|
| 6 |
+
|
| 7 |
+
interface WeatherServiceClient {
|
| 8 |
+
GetCurrentWeather(data: { city: string; country: string }): Observable<any>;
|
| 9 |
+
GetForecast(data: { city: string; country: string; days: number }): Observable<any>;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
@Injectable()
|
| 13 |
+
export class WeatherService implements OnModuleInit {
|
| 14 |
+
private weatherService: WeatherServiceClient;
|
| 15 |
+
|
| 16 |
+
private currentWeatherBreaker: opossum<any, any>;
|
| 17 |
+
private forecastBreaker: opossum<any, any>;
|
| 18 |
+
|
| 19 |
+
constructor(@Inject('WEATHER_API_PACKAGE') private readonly client: ClientGrpc) {}
|
| 20 |
+
|
| 21 |
+
onModuleInit() {
|
| 22 |
+
this.weatherService = this.client.getService<WeatherServiceClient>('WeatherApiService');
|
| 23 |
+
|
| 24 |
+
// Circuit breaker for GetCurrentWeather
|
| 25 |
+
this.currentWeatherBreaker = new opossum(
|
| 26 |
+
(data: { city: string; country: string }) =>
|
| 27 |
+
lastValueFrom(this.weatherService.GetCurrentWeather(data)),
|
| 28 |
+
{
|
| 29 |
+
timeout: 5000, // 5 seconds timeout
|
| 30 |
+
errorThresholdPercentage: 50,
|
| 31 |
+
resetTimeout: 10000, // 10 seconds before retry
|
| 32 |
+
}
|
| 33 |
+
);
|
| 34 |
+
|
| 35 |
+
// Circuit breaker for GetForecast
|
| 36 |
+
this.forecastBreaker = new opossum(
|
| 37 |
+
(data: { city: string; country: string; days: number }) =>
|
| 38 |
+
lastValueFrom(this.weatherService.GetForecast(data)),
|
| 39 |
+
{
|
| 40 |
+
timeout: 5000,
|
| 41 |
+
errorThresholdPercentage: 50,
|
| 42 |
+
resetTimeout: 10000,
|
| 43 |
+
}
|
| 44 |
+
);
|
| 45 |
+
|
| 46 |
+
this.currentWeatherBreaker.on('open', () => console.warn('⚠️ GetCurrentWeather breaker opened'));
|
| 47 |
+
this.currentWeatherBreaker.on('close', () => console.log('✅ GetCurrentWeather breaker closed'));
|
| 48 |
+
|
| 49 |
+
this.forecastBreaker.on('open', () => console.warn('⚠️ GetForecast breaker opened'));
|
| 50 |
+
this.forecastBreaker.on('close', () => console.log('✅ GetForecast breaker closed'));
|
| 51 |
+
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
async getCurrentWeather(city: string, country: string) {
|
| 55 |
+
return this.currentWeatherBreaker.fire({ city, country });
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
async getForecast(city: string, country: string, days: number) {
|
| 59 |
+
return this.forecastBreaker.fire({ city, country, days });
|
| 60 |
+
}
|
| 61 |
+
}
|
apps/apigateway/src/service-map.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"/proxy/customers": "http://localhost:3002",
|
| 3 |
+
"/oauth": "http://localhost:3004",
|
| 4 |
+
"/weather": "http://localhost:3005",
|
| 5 |
+
"default": "http://localhost:3000"
|
| 6 |
+
}
|
| 7 |
+
|
apps/apigateway/test/app.e2e-spec.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Test, TestingModule } from '@nestjs/testing';
|
| 2 |
+
import { INestApplication } from '@nestjs/common';
|
| 3 |
+
import * as request from 'supertest';
|
| 4 |
+
import { ApigatewayModule } from './../src/apigateway.module';
|
| 5 |
+
|
| 6 |
+
describe('ApigatewayController (e2e)', () => {
|
| 7 |
+
let app: INestApplication;
|
| 8 |
+
|
| 9 |
+
beforeEach(async () => {
|
| 10 |
+
const moduleFixture: TestingModule = await Test.createTestingModule({
|
| 11 |
+
imports: [ApigatewayModule],
|
| 12 |
+
}).compile();
|
| 13 |
+
|
| 14 |
+
app = moduleFixture.createNestApplication();
|
| 15 |
+
await app.init();
|
| 16 |
+
});
|
| 17 |
+
|
| 18 |
+
it('/ (GET)', () => {
|
| 19 |
+
return request(app.getHttpServer())
|
| 20 |
+
.get('/')
|
| 21 |
+
.expect(200)
|
| 22 |
+
.expect('Hello World!');
|
| 23 |
+
});
|
| 24 |
+
});
|
apps/apigateway/test/jest-e2e.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"moduleFileExtensions": ["js", "json", "ts"],
|
| 3 |
+
"rootDir": ".",
|
| 4 |
+
"testEnvironment": "node",
|
| 5 |
+
"testRegex": ".e2e-spec.ts$",
|
| 6 |
+
"transform": {
|
| 7 |
+
"^.+\\.(t|j)s$": "ts-jest"
|
| 8 |
+
}
|
| 9 |
+
}
|
apps/apigateway/tsconfig.app.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"extends": "../../tsconfig.json",
|
| 3 |
+
"compilerOptions": {
|
| 4 |
+
"declaration": false,
|
| 5 |
+
"outDir": "../../dist/apps/apigateway",
|
| 6 |
+
"sourceMap": false
|
| 7 |
+
},
|
| 8 |
+
"include": ["src/**/*"],
|
| 9 |
+
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
|
| 10 |
+
}
|
apps/azureapi/src/azureapi.controller.spec.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Test, TestingModule } from '@nestjs/testing';
|
| 2 |
+
import { AzureapiController } from './azureapi.controller';
|
| 3 |
+
import { AzureapiService } from './azureapi.service';
|
| 4 |
+
|
| 5 |
+
describe('AzureapiController', () => {
|
| 6 |
+
let azureapiController: AzureapiController;
|
| 7 |
+
|
| 8 |
+
beforeEach(async () => {
|
| 9 |
+
const app: TestingModule = await Test.createTestingModule({
|
| 10 |
+
controllers: [AzureapiController],
|
| 11 |
+
providers: [AzureapiService],
|
| 12 |
+
}).compile();
|
| 13 |
+
|
| 14 |
+
azureapiController = app.get<AzureapiController>(AzureapiController);
|
| 15 |
+
});
|
| 16 |
+
|
| 17 |
+
describe('root', () => {
|
| 18 |
+
it('should return "Hello World!"', () => {
|
| 19 |
+
expect(azureapiController.getHello()).toBe('Hello World!');
|
| 20 |
+
});
|
| 21 |
+
});
|
| 22 |
+
});
|
apps/azureapi/src/azureapi.controller.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// azureapi.controller.ts
|
| 2 |
+
import { Controller } from '@nestjs/common';
|
| 3 |
+
import { GrpcMethod } from '@nestjs/microservices';
|
| 4 |
+
import { AzureApiService } from './azureapi.service';
|
| 5 |
+
|
| 6 |
+
@Controller()
|
| 7 |
+
export class AzureApiController {
|
| 8 |
+
constructor(private readonly azureApiService: AzureApiService) {}
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@GrpcMethod('AzureApiService', 'readSecret') // <== Matches proto service & method
|
| 12 |
+
readSecret(data: { kvName: string; secretName: string }) {
|
| 13 |
+
const { kvName, secretName } = data;
|
| 14 |
+
return this.azureApiService.readSecret({ kvName, secretName });
|
| 15 |
+
}
|
| 16 |
+
}
|
apps/azureapi/src/azureapi.module.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Module } from '@nestjs/common';
|
| 2 |
+
import { AzureApiController } from './azureapi.controller';
|
| 3 |
+
import { AzureApiService } from './azureapi.service';
|
| 4 |
+
import { AzureTokenService } from '@bpm/common';
|
| 5 |
+
|
| 6 |
+
@Module({
|
| 7 |
+
imports: [],
|
| 8 |
+
controllers: [AzureApiController],
|
| 9 |
+
providers: [AzureApiService, AzureTokenService],
|
| 10 |
+
})
|
| 11 |
+
export class AzureapiModule {}
|
apps/azureapi/src/azureapi.service.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable } from '@nestjs/common';
|
| 2 |
+
import { AzureTokenService } from '@bpm/common';
|
| 3 |
+
import { SecretClient } from '@azure/keyvault-secrets';
|
| 4 |
+
|
| 5 |
+
@Injectable()
|
| 6 |
+
export class AzureApiService {
|
| 7 |
+
constructor(private readonly azureTokenService: AzureTokenService) {}
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
readSecret({ kvName, secretName }: { kvName: string; secretName: string }) {
|
| 11 |
+
//console.log('Token:', this.azureTokenService.getToken());
|
| 12 |
+
const credential = this.azureTokenService.getTokenCredential();
|
| 13 |
+
if (!credential) {
|
| 14 |
+
console.error('Token is not available');
|
| 15 |
+
return null;
|
| 16 |
+
}
|
| 17 |
+
else {
|
| 18 |
+
return {
|
| 19 |
+
kvName,
|
| 20 |
+
secretName,
|
| 21 |
+
secretValue: `Value-of-${secretName}-from-${kvName}`,
|
| 22 |
+
};
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
}
|
apps/azureapi/src/main.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NestFactory } from '@nestjs/core';
|
| 2 |
+
import { AzureapiModule } from './azureapi.module';
|
| 3 |
+
import { Transport, MicroserviceOptions } from '@nestjs/microservices';
|
| 4 |
+
import { join } from 'path';
|
| 5 |
+
import { ApploggerService } from '@bpm/common';
|
| 6 |
+
import * as dotenv from 'dotenv';
|
| 7 |
+
|
| 8 |
+
// Load environment variables from .env file
|
| 9 |
+
dotenv.config();
|
| 10 |
+
|
| 11 |
+
async function bootstrap() {
|
| 12 |
+
const context = 'azureapi';
|
| 13 |
+
const logger = new ApploggerService(context);
|
| 14 |
+
|
| 15 |
+
const isProd = process.env.NODE_ENV === 'production';
|
| 16 |
+
const protoPath = isProd
|
| 17 |
+
? '../../libs/proto/azureapi.proto'
|
| 18 |
+
: '../../../libs/proto/azureapi.proto';
|
| 19 |
+
|
| 20 |
+
//console.log('protoPath', join(__dirname, protoPath));
|
| 21 |
+
|
| 22 |
+
const port = process.env.PORT || 3021;
|
| 23 |
+
const hostingURL = 'localhost:' + port;
|
| 24 |
+
|
| 25 |
+
NestFactory.createMicroservice<MicroserviceOptions>(AzureapiModule, {
|
| 26 |
+
transport: Transport.GRPC,
|
| 27 |
+
options: {
|
| 28 |
+
package: 'azureapi',
|
| 29 |
+
protoPath: join(__dirname, protoPath),
|
| 30 |
+
url: hostingURL,
|
| 31 |
+
},
|
| 32 |
+
})
|
| 33 |
+
.then(app => {
|
| 34 |
+
logger.log('🛠️ Starting up the NestJS Application...\n', context);
|
| 35 |
+
|
| 36 |
+
return app.listen().then(() => {
|
| 37 |
+
logger.log('✅ App Modules initialized successfully\n', context);
|
| 38 |
+
logger.log('🌐 Enabling global configurations...\n', context);
|
| 39 |
+
logger.log('📡 Ready to accept incoming requests!\n', context);
|
| 40 |
+
logger.log('🧠 Powered by NestJS ❤️\n', context);
|
| 41 |
+
});
|
| 42 |
+
})
|
| 43 |
+
.catch(err => {
|
| 44 |
+
logger.error('❌ Failed to start WeatherAPI microservice: ' + err, context);
|
| 45 |
+
process.exit(1);
|
| 46 |
+
});
|
| 47 |
+
|
| 48 |
+
}
|
| 49 |
+
bootstrap();
|
apps/azureapi/test/app.e2e-spec.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Test, TestingModule } from '@nestjs/testing';
|
| 2 |
+
import { INestApplication } from '@nestjs/common';
|
| 3 |
+
import * as request from 'supertest';
|
| 4 |
+
import { AzureapiModule } from './../src/azureapi.module';
|
| 5 |
+
|
| 6 |
+
describe('AzureapiController (e2e)', () => {
|
| 7 |
+
let app: INestApplication;
|
| 8 |
+
|
| 9 |
+
beforeEach(async () => {
|
| 10 |
+
const moduleFixture: TestingModule = await Test.createTestingModule({
|
| 11 |
+
imports: [AzureapiModule],
|
| 12 |
+
}).compile();
|
| 13 |
+
|
| 14 |
+
app = moduleFixture.createNestApplication();
|
| 15 |
+
await app.init();
|
| 16 |
+
});
|
| 17 |
+
|
| 18 |
+
it('/ (GET)', () => {
|
| 19 |
+
return request(app.getHttpServer())
|
| 20 |
+
.get('/')
|
| 21 |
+
.expect(200)
|
| 22 |
+
.expect('Hello World!');
|
| 23 |
+
});
|
| 24 |
+
});
|
apps/azureapi/test/jest-e2e.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"moduleFileExtensions": ["js", "json", "ts"],
|
| 3 |
+
"rootDir": ".",
|
| 4 |
+
"testEnvironment": "node",
|
| 5 |
+
"testRegex": ".e2e-spec.ts$",
|
| 6 |
+
"transform": {
|
| 7 |
+
"^.+\\.(t|j)s$": "ts-jest"
|
| 8 |
+
}
|
| 9 |
+
}
|
apps/azureapi/tsconfig.app.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"extends": "../../tsconfig.json",
|
| 3 |
+
"compilerOptions": {
|
| 4 |
+
"declaration": false,
|
| 5 |
+
"outDir": "../../dist/apps/azureapi"
|
| 6 |
+
},
|
| 7 |
+
"include": ["src/**/*"],
|
| 8 |
+
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
|
| 9 |
+
}
|
apps/northwindapi/src/config/typeorm.config.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { DataSource } from 'typeorm';
|
| 2 |
+
import * as Entities from '@bpm/data/models';
|
| 3 |
+
import { config } from 'dotenv';
|
| 4 |
+
config(); // Ensure environment variables are loaded
|
| 5 |
+
import * as path from 'path';
|
| 6 |
+
|
| 7 |
+
// console.log('Database URL:', process.env.DATABASE_URL);
|
| 8 |
+
|
| 9 |
+
const appRoot = path.resolve(__dirname, '..', '..');
|
| 10 |
+
console.log('App root:', appRoot);
|
| 11 |
+
|
| 12 |
+
const dbUrl = new URL(process.env.DATABASE_URL || '');
|
| 13 |
+
// const appDirectory = process.cwd();
|
| 14 |
+
const isDev = process.env.NODE_ENV !== 'production';
|
| 15 |
+
// const sourceDir = isDev ? 'src' : 'dist';
|
| 16 |
+
|
| 17 |
+
// console.log('Current directory:', appDirectory);
|
| 18 |
+
// console.log('Root directory:', rootDir);
|
| 19 |
+
// console.log('isDev:', isDev);
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
// const entitiesPath = path.join(
|
| 23 |
+
// appRoot,
|
| 24 |
+
// "src",
|
| 25 |
+
// 'models',
|
| 26 |
+
// `*.entity.${isDev ? 'ts' : 'js'}`,
|
| 27 |
+
// );
|
| 28 |
+
// const migrationsPath = path.join(
|
| 29 |
+
// appRoot,
|
| 30 |
+
// "src",
|
| 31 |
+
// 'database',
|
| 32 |
+
// 'migrations',
|
| 33 |
+
// `*-migration.${isDev ? 'ts' : 'js'}`,
|
| 34 |
+
// );
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
// const entities = path.join(
|
| 38 |
+
// appRoot,
|
| 39 |
+
// "src",
|
| 40 |
+
// 'models',
|
| 41 |
+
// `*.entity.${isDev ? 'ts' : 'js'}`,
|
| 42 |
+
// );
|
| 43 |
+
const migrations = path.join(
|
| 44 |
+
appRoot,
|
| 45 |
+
"src",
|
| 46 |
+
'database',
|
| 47 |
+
'migrations',
|
| 48 |
+
`*-migration.${isDev ? 'ts' : 'js'}`,
|
| 49 |
+
);
|
| 50 |
+
|
| 51 |
+
// console.log('Entities path:', entities);
|
| 52 |
+
// console.log('Migrations path:', migrations);
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
const AppDataSource = new DataSource({
|
| 57 |
+
type: 'postgres',
|
| 58 |
+
host: dbUrl.hostname,
|
| 59 |
+
port: Number(dbUrl.port) || 5432, // Default to 5432 if not set
|
| 60 |
+
username: dbUrl.username,
|
| 61 |
+
password: dbUrl.password,
|
| 62 |
+
database: dbUrl.pathname.slice(1), // Remove the leading slash from the database name
|
| 63 |
+
// ssl: dbUrl.protocol === 'postgresqls:', // Use SSL if the protocol is postgresqls
|
| 64 |
+
ssl: {
|
| 65 |
+
rejectUnauthorized: false,
|
| 66 |
+
},
|
| 67 |
+
extra: {
|
| 68 |
+
ssl: {
|
| 69 |
+
rejectUnauthorized: false,
|
| 70 |
+
},
|
| 71 |
+
},
|
| 72 |
+
// entities: [entities],
|
| 73 |
+
// migrations: [migrations],
|
| 74 |
+
entities: Object.values(Entities),
|
| 75 |
+
migrations: [migrations],
|
| 76 |
+
synchronize: false, // Set to false in production
|
| 77 |
+
migrationsRun: false,
|
| 78 |
+
logging: false,
|
| 79 |
+
});
|
| 80 |
+
|
| 81 |
+
export default AppDataSource;
|
apps/northwindapi/src/database/migrations/1745487689382-migration.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { MigrationInterface, QueryRunner } from 'typeorm';
|
| 2 |
+
|
| 3 |
+
export class Migration1745487689382 implements MigrationInterface {
|
| 4 |
+
name = 'Migration1745487689382';
|
| 5 |
+
|
| 6 |
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
| 7 |
+
await queryRunner.query(
|
| 8 |
+
`CREATE TABLE "Shippers" ("ShipperID" SERIAL NOT NULL, "CompanyName" character varying(40) NOT NULL, "Phone" character varying(24), "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_475819881aa030a55cbb4dfa077" PRIMARY KEY ("ShipperID"))`,
|
| 9 |
+
);
|
| 10 |
+
await queryRunner.query(
|
| 11 |
+
`CREATE TABLE "Customers" ("CustomerID" character(5) NOT NULL, "CompanyName" character varying(40) NOT NULL, "ContactName" character varying(30), "ContactTitle" character varying(30), "Address" character varying(60), "City" character varying(15), "Region" character varying(15), "PostalCode" character varying(10), "Country" character varying(15), "Phone" character varying(24), "Fax" character varying(24), "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_20d9e62f5dfe25e72bc90e46257" PRIMARY KEY ("CustomerID"))`,
|
| 12 |
+
);
|
| 13 |
+
await queryRunner.query(
|
| 14 |
+
`CREATE TABLE "Employees" ("EmployeeID" SERIAL NOT NULL, "LastName" character varying(20) NOT NULL, "FirstName" character varying(10) NOT NULL, "Title" character varying(30), "TitleOfCourtesy" character varying(25), "BirthDate" TIMESTAMP, "HireDate" TIMESTAMP, "Address" character varying(60), "City" character varying(15), "Region" character varying(15), "PostalCode" character varying(10), "Country" character varying(15), "HomePhone" character varying(24), "Extension" character varying(4), "Notes" text, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "ReportsTo" integer, CONSTRAINT "PK_31149b984f38111c8faf85124c7" PRIMARY KEY ("EmployeeID"))`,
|
| 15 |
+
);
|
| 16 |
+
await queryRunner.query(
|
| 17 |
+
`CREATE TABLE "Orders" ("OrderID" SERIAL NOT NULL, "OrderDate" TIMESTAMP, "RequiredDate" TIMESTAMP, "ShippedDate" TIMESTAMP, "Freight" numeric(18,2), "ShipName" character varying(60), "ShipAddress" character varying(60), "ShipCity" character varying(15), "ShipRegion" character varying(15), "ShipPostalCode" character varying(10), "ShipCountry" character varying(15), "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "CustomerID" character(5), "EmployeeID" integer, "ShipVia" integer, CONSTRAINT "PK_55f8443f4d79e9a848cf42b69d9" PRIMARY KEY ("OrderID"))`,
|
| 18 |
+
);
|
| 19 |
+
await queryRunner.query(
|
| 20 |
+
`CREATE TABLE "Suppliers" ("SupplierID" SERIAL NOT NULL, "CompanyName" character varying(40) NOT NULL, "ContactName" character varying(30), "ContactTitle" character varying(30), "Address" character varying(60), "City" character varying(15), "Region" character varying(15), "PostalCode" character varying(10), "Country" character varying(15), "Phone" character varying(24), "Fax" character varying(24), "HomePage" text, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_9af75be88249fe42e9a8fb47629" PRIMARY KEY ("SupplierID"))`,
|
| 21 |
+
);
|
| 22 |
+
await queryRunner.query(
|
| 23 |
+
`CREATE TABLE "Categories" ("CategoryID" SERIAL NOT NULL, "CategoryName" character varying(15) NOT NULL, "Description" text, "Picture" bytea, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_8fb0727baad4afaed25ac7c9861" PRIMARY KEY ("CategoryID"))`,
|
| 24 |
+
);
|
| 25 |
+
await queryRunner.query(
|
| 26 |
+
`CREATE TABLE "Products" ("ProductID" SERIAL NOT NULL, "ProductName" character varying(40) NOT NULL, "QuantityPerUnit" character varying(20), "UnitPrice" numeric(18,2), "UnitsInStock" smallint, "UnitsOnOrder" smallint, "ReorderLevel" smallint, "Discontinued" boolean NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "SupplierID" integer, "CategoryID" integer, CONSTRAINT "PK_07f84037390838453c1426c7cb5" PRIMARY KEY ("ProductID"))`,
|
| 27 |
+
);
|
| 28 |
+
await queryRunner.query(
|
| 29 |
+
`CREATE TABLE "OrderDetails" ("OrderDetailID" SERIAL NOT NULL, "UnitPrice" numeric(18,2) NOT NULL, "Quantity" smallint NOT NULL, "Discount" real NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "OrderID" integer, "ProductID" integer, CONSTRAINT "PK_62169a0638c62361a12768293f8" PRIMARY KEY ("OrderDetailID"))`,
|
| 30 |
+
);
|
| 31 |
+
await queryRunner.query(
|
| 32 |
+
`ALTER TABLE "Employees" ADD CONSTRAINT "FK_0a2e430ee66d427d2fd728ce671" FOREIGN KEY ("ReportsTo") REFERENCES "Employees"("EmployeeID") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
| 33 |
+
);
|
| 34 |
+
await queryRunner.query(
|
| 35 |
+
`ALTER TABLE "Orders" ADD CONSTRAINT "FK_fcb27b11e453edc543d0a5436eb" FOREIGN KEY ("CustomerID") REFERENCES "Customers"("CustomerID") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
| 36 |
+
);
|
| 37 |
+
await queryRunner.query(
|
| 38 |
+
`ALTER TABLE "Orders" ADD CONSTRAINT "FK_4ef049bfb564e1dbab6b55d9503" FOREIGN KEY ("EmployeeID") REFERENCES "Employees"("EmployeeID") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
| 39 |
+
);
|
| 40 |
+
await queryRunner.query(
|
| 41 |
+
`ALTER TABLE "Orders" ADD CONSTRAINT "FK_bd8071f28699758415c62dfd7be" FOREIGN KEY ("ShipVia") REFERENCES "Shippers"("ShipperID") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
| 42 |
+
);
|
| 43 |
+
await queryRunner.query(
|
| 44 |
+
`ALTER TABLE "Products" ADD CONSTRAINT "FK_3631250b029818892d266a3a0a8" FOREIGN KEY ("SupplierID") REFERENCES "Suppliers"("SupplierID") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
| 45 |
+
);
|
| 46 |
+
await queryRunner.query(
|
| 47 |
+
`ALTER TABLE "Products" ADD CONSTRAINT "FK_9d404e9029f724e36f0ce2f0024" FOREIGN KEY ("CategoryID") REFERENCES "Categories"("CategoryID") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
| 48 |
+
);
|
| 49 |
+
await queryRunner.query(
|
| 50 |
+
`ALTER TABLE "OrderDetails" ADD CONSTRAINT "FK_36af61326d32a5b6853c79642f1" FOREIGN KEY ("OrderID") REFERENCES "Orders"("OrderID") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
| 51 |
+
);
|
| 52 |
+
await queryRunner.query(
|
| 53 |
+
`ALTER TABLE "OrderDetails" ADD CONSTRAINT "FK_2dd62647d008dcfcc7846aee102" FOREIGN KEY ("ProductID") REFERENCES "Products"("ProductID") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
| 54 |
+
);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
| 58 |
+
await queryRunner.query(
|
| 59 |
+
`ALTER TABLE "OrderDetails" DROP CONSTRAINT "FK_2dd62647d008dcfcc7846aee102"`,
|
| 60 |
+
);
|
| 61 |
+
await queryRunner.query(
|
| 62 |
+
`ALTER TABLE "OrderDetails" DROP CONSTRAINT "FK_36af61326d32a5b6853c79642f1"`,
|
| 63 |
+
);
|
| 64 |
+
await queryRunner.query(
|
| 65 |
+
`ALTER TABLE "Products" DROP CONSTRAINT "FK_9d404e9029f724e36f0ce2f0024"`,
|
| 66 |
+
);
|
| 67 |
+
await queryRunner.query(
|
| 68 |
+
`ALTER TABLE "Products" DROP CONSTRAINT "FK_3631250b029818892d266a3a0a8"`,
|
| 69 |
+
);
|
| 70 |
+
await queryRunner.query(
|
| 71 |
+
`ALTER TABLE "Orders" DROP CONSTRAINT "FK_bd8071f28699758415c62dfd7be"`,
|
| 72 |
+
);
|
| 73 |
+
await queryRunner.query(
|
| 74 |
+
`ALTER TABLE "Orders" DROP CONSTRAINT "FK_4ef049bfb564e1dbab6b55d9503"`,
|
| 75 |
+
);
|
| 76 |
+
await queryRunner.query(
|
| 77 |
+
`ALTER TABLE "Orders" DROP CONSTRAINT "FK_fcb27b11e453edc543d0a5436eb"`,
|
| 78 |
+
);
|
| 79 |
+
await queryRunner.query(
|
| 80 |
+
`ALTER TABLE "Employees" DROP CONSTRAINT "FK_0a2e430ee66d427d2fd728ce671"`,
|
| 81 |
+
);
|
| 82 |
+
await queryRunner.query(`DROP TABLE "OrderDetails"`);
|
| 83 |
+
await queryRunner.query(`DROP TABLE "Products"`);
|
| 84 |
+
await queryRunner.query(`DROP TABLE "Categories"`);
|
| 85 |
+
await queryRunner.query(`DROP TABLE "Suppliers"`);
|
| 86 |
+
await queryRunner.query(`DROP TABLE "Orders"`);
|
| 87 |
+
await queryRunner.query(`DROP TABLE "Employees"`);
|
| 88 |
+
await queryRunner.query(`DROP TABLE "Customers"`);
|
| 89 |
+
await queryRunner.query(`DROP TABLE "Shippers"`);
|
| 90 |
+
}
|
| 91 |
+
}
|
apps/northwindapi/src/database/migrations/1748636404589-migration.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
| 2 |
+
|
| 3 |
+
export class Migration1748636404589 implements MigrationInterface {
|
| 4 |
+
name = 'Migration1748636404589'
|
| 5 |
+
|
| 6 |
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
| 7 |
+
await queryRunner.query(`ALTER TABLE "Customers" ADD "PhotoURL" character varying(60)`);
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
| 11 |
+
await queryRunner.query(`ALTER TABLE "Customers" DROP COLUMN "PhotoURL"`);
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
}
|
apps/northwindapi/src/database/seed/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import AppDataSource from '../../config/typeorm.config';
|
| 2 |
+
import { seedCategories } from './seed-categories';
|
| 3 |
+
import { seedShippers } from './seed-shippers';
|
| 4 |
+
import { seedCustomers } from './seed-customers';
|
| 5 |
+
import { seedEmployees } from './seed-employees';
|
| 6 |
+
import { seedSuppliers } from './seed-suppliers';
|
| 7 |
+
import { seedProducts } from './seed-products';
|
| 8 |
+
import { seedOrders } from './seed-orders';
|
| 9 |
+
import { orderDetailSeed } from './seed-order-details';
|
| 10 |
+
|
| 11 |
+
async function runSeeding() {
|
| 12 |
+
try {
|
| 13 |
+
await AppDataSource.initialize();
|
| 14 |
+
console.log('🚀 DataSource initialized');
|
| 15 |
+
|
| 16 |
+
// Debug: List the loaded entities
|
| 17 |
+
console.log(AppDataSource.entityMetadatas);
|
| 18 |
+
|
| 19 |
+
await seedCategories(AppDataSource);
|
| 20 |
+
await seedShippers(AppDataSource);
|
| 21 |
+
await seedCustomers(AppDataSource);
|
| 22 |
+
await seedEmployees(AppDataSource);
|
| 23 |
+
await seedSuppliers(AppDataSource);
|
| 24 |
+
await seedProducts(AppDataSource);
|
| 25 |
+
await seedOrders(AppDataSource);
|
| 26 |
+
await orderDetailSeed(AppDataSource);
|
| 27 |
+
|
| 28 |
+
await AppDataSource.destroy();
|
| 29 |
+
console.log('✅ Seeding completed, connection closed');
|
| 30 |
+
} catch (error) {
|
| 31 |
+
console.error('❌ Error during seeding:', error);
|
| 32 |
+
process.exit(1);
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
runSeeding();
|
apps/northwindapi/src/database/seed/seed-categories.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { DataSource } from 'typeorm';
|
| 2 |
+
import { Category } from '@bpm/data/models/category.entity';
|
| 3 |
+
|
| 4 |
+
export async function seedCategories(dataSource: DataSource) {
|
| 5 |
+
const repo = dataSource.getRepository(Category);
|
| 6 |
+
const existing = await repo.count();
|
| 7 |
+
if (existing > 0) {
|
| 8 |
+
console.log('🛑 Categories already exist. Skipping...');
|
| 9 |
+
return;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
const categories = [
|
| 13 |
+
{ CategoryName: 'Beverages', Description: 'Soft drinks, tea, coffee' },
|
| 14 |
+
{ CategoryName: 'Condiments', Description: 'Sauces and seasonings' },
|
| 15 |
+
{ CategoryName: 'Confections', Description: 'Desserts and sweet breads' },
|
| 16 |
+
{ CategoryName: 'Dairy Products', Description: 'Cheeses and other dairy' },
|
| 17 |
+
{ CategoryName: 'Grains/Cereals', Description: 'Breads, crackers, pasta' },
|
| 18 |
+
{ CategoryName: 'Meat/Poultry', Description: 'Prepared meats' },
|
| 19 |
+
{
|
| 20 |
+
CategoryName: 'Produce',
|
| 21 |
+
Description: 'Dried and fresh fruit/vegetables',
|
| 22 |
+
},
|
| 23 |
+
{ CategoryName: 'Seafood', Description: 'Seaweed, fish' },
|
| 24 |
+
{ CategoryName: 'Snacks', Description: 'Chips and small packs' },
|
| 25 |
+
{ CategoryName: 'Health', Description: 'Supplements and vitamins' },
|
| 26 |
+
];
|
| 27 |
+
|
| 28 |
+
await repo.save(categories);
|
| 29 |
+
console.log('✅ Seeded categories');
|
| 30 |
+
}
|
apps/northwindapi/src/database/seed/seed-customers.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// src/database/seed/seed-customers.ts
|
| 2 |
+
import { DataSource } from 'typeorm';
|
| 3 |
+
import { Customer } from '@bpm/data/models/customer.entity';
|
| 4 |
+
|
| 5 |
+
export async function seedCustomers(dataSource: DataSource) {
|
| 6 |
+
const repo = dataSource.getRepository(Customer);
|
| 7 |
+
const existing = await repo.count();
|
| 8 |
+
if (existing > 0) {
|
| 9 |
+
console.log('🛑 Customers already exist. Skipping...');
|
| 10 |
+
return;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
const customers: Partial<Customer>[] = [
|
| 14 |
+
{
|
| 15 |
+
CustomerID: 'ALFKI',
|
| 16 |
+
CompanyName: 'Alfreds Futterkiste',
|
| 17 |
+
ContactName: 'Maria Anders',
|
| 18 |
+
ContactTitle: 'Sales Representative',
|
| 19 |
+
Address: 'Obere Str. 57',
|
| 20 |
+
City: 'Berlin',
|
| 21 |
+
PostalCode: '12209',
|
| 22 |
+
Country: 'Germany',
|
| 23 |
+
Phone: '030-0074321',
|
| 24 |
+
Fax: '030-0076545',
|
| 25 |
+
PhotoURL: 'https://randomuser.me/api/portraits/men/1.jpg'
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
CustomerID: 'ANATR',
|
| 29 |
+
CompanyName: 'Ana Trujillo Emparedados y helados',
|
| 30 |
+
ContactName: 'Ana Trujillo',
|
| 31 |
+
ContactTitle: 'Owner',
|
| 32 |
+
Address: 'Avda. de la Constitución 2222',
|
| 33 |
+
City: 'México D.F.',
|
| 34 |
+
PostalCode: '05021',
|
| 35 |
+
Country: 'Mexico',
|
| 36 |
+
Phone: '(5) 555-4729',
|
| 37 |
+
Fax: '(5) 555-3745',
|
| 38 |
+
PhotoURL: 'https://randomuser.me/api/portraits/men/2.jpg'
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
CustomerID: 'ANTON',
|
| 42 |
+
CompanyName: 'Antonio Moreno Taquería',
|
| 43 |
+
ContactName: 'Antonio Moreno',
|
| 44 |
+
ContactTitle: 'Owner',
|
| 45 |
+
Address: 'Mataderos 2312',
|
| 46 |
+
City: 'México D.F.',
|
| 47 |
+
PostalCode: '05023',
|
| 48 |
+
Country: 'Mexico',
|
| 49 |
+
Phone: '(5) 555-3932',
|
| 50 |
+
PhotoURL: 'https://randomuser.me/api/portraits/women/3.jpg'
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
CustomerID: 'AROUT',
|
| 54 |
+
CompanyName: 'Around the Horn',
|
| 55 |
+
ContactName: 'Thomas Hardy',
|
| 56 |
+
ContactTitle: 'Sales Representative',
|
| 57 |
+
Address: '120 Hanover Sq.',
|
| 58 |
+
City: 'London',
|
| 59 |
+
PostalCode: 'WA1 1DP',
|
| 60 |
+
Country: 'UK',
|
| 61 |
+
Phone: '(171) 555-7788',
|
| 62 |
+
Fax: '(171) 555-6750',
|
| 63 |
+
PhotoURL: 'https://randomuser.me/api/portraits/men/4.jpg'
|
| 64 |
+
},
|
| 65 |
+
{
|
| 66 |
+
CustomerID: 'BERGS',
|
| 67 |
+
CompanyName: 'Berglunds snabbköp',
|
| 68 |
+
ContactName: 'Christina Berglund',
|
| 69 |
+
ContactTitle: 'Order Administrator',
|
| 70 |
+
Address: 'Berguvsvägen 8',
|
| 71 |
+
City: 'Luleå',
|
| 72 |
+
PostalCode: 'S-958 22',
|
| 73 |
+
Country: 'Sweden',
|
| 74 |
+
Phone: '0921-12 34 65',
|
| 75 |
+
Fax: '0921-12 34 67',
|
| 76 |
+
PhotoURL: 'https://randomuser.me/api/portraits/women/5.jpg'
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
CustomerID: 'BLAUS',
|
| 80 |
+
CompanyName: 'Blauer See Delikatessen',
|
| 81 |
+
ContactName: 'Hanna Moos',
|
| 82 |
+
ContactTitle: 'Sales Representative',
|
| 83 |
+
Address: 'Forsterstr. 57',
|
| 84 |
+
City: 'Mannheim',
|
| 85 |
+
PostalCode: '68306',
|
| 86 |
+
Country: 'Germany',
|
| 87 |
+
Phone: '0621-08460',
|
| 88 |
+
Fax: '0621-08924',
|
| 89 |
+
PhotoURL: 'https://randomuser.me/api/portraits/women/6.jpg'
|
| 90 |
+
},
|
| 91 |
+
{
|
| 92 |
+
CustomerID: 'BLONP',
|
| 93 |
+
CompanyName: 'Blondel père et fils',
|
| 94 |
+
ContactName: 'Frédérique Citeaux',
|
| 95 |
+
ContactTitle: 'Marketing Manager',
|
| 96 |
+
Address: '24, place Kléber',
|
| 97 |
+
City: 'Strasbourg',
|
| 98 |
+
PostalCode: '67000',
|
| 99 |
+
Country: 'France',
|
| 100 |
+
Phone: '88.60.15.31',
|
| 101 |
+
Fax: '88.60.15.32',
|
| 102 |
+
PhotoURL: 'https://randomuser.me/api/portraits/men/7.jpg'
|
| 103 |
+
},
|
| 104 |
+
{
|
| 105 |
+
CustomerID: 'BOLID',
|
| 106 |
+
CompanyName: 'Bólido Comidas preparadas',
|
| 107 |
+
ContactName: 'Martín Sommer',
|
| 108 |
+
ContactTitle: 'Owner',
|
| 109 |
+
Address: 'C/ Araquil, 67',
|
| 110 |
+
City: 'Madrid',
|
| 111 |
+
PostalCode: '28023',
|
| 112 |
+
Country: 'Spain',
|
| 113 |
+
Phone: '(91) 555 22 82',
|
| 114 |
+
Fax: '(91) 555 91 99',
|
| 115 |
+
PhotoURL: 'https://randomuser.me/api/portraits/women/8.jpg'
|
| 116 |
+
},
|
| 117 |
+
{
|
| 118 |
+
CustomerID: 'BONAP',
|
| 119 |
+
CompanyName: "Bon app'",
|
| 120 |
+
ContactName: 'Laurence Lebihan',
|
| 121 |
+
ContactTitle: 'Owner',
|
| 122 |
+
Address: '12, rue des Bouchers',
|
| 123 |
+
City: 'Marseille',
|
| 124 |
+
PostalCode: '13008',
|
| 125 |
+
Country: 'France',
|
| 126 |
+
Phone: '91.24.45.40',
|
| 127 |
+
Fax: '91.24.45.41',
|
| 128 |
+
PhotoURL: 'https://randomuser.me/api/portraits/women/9.jpg'
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
CustomerID: 'BOTTM',
|
| 132 |
+
CompanyName: 'Bottom-Dollar Markets',
|
| 133 |
+
ContactName: 'Elizabeth Lincoln',
|
| 134 |
+
ContactTitle: 'Accounting Manager',
|
| 135 |
+
Address: '23 Tsawassen Blvd.',
|
| 136 |
+
City: 'Tsawassen',
|
| 137 |
+
Region: 'BC',
|
| 138 |
+
PostalCode: 'T2F 8M4',
|
| 139 |
+
Country: 'Canada',
|
| 140 |
+
Phone: '(604) 555-4729',
|
| 141 |
+
Fax: '(604) 555-3745',
|
| 142 |
+
PhotoURL: 'https://randomuser.me/api/portraits/men/10.jpg'
|
| 143 |
+
},
|
| 144 |
+
];
|
| 145 |
+
|
| 146 |
+
await repo.save(customers);
|
| 147 |
+
console.log('✅ Seeded customers');
|
| 148 |
+
}
|
apps/northwindapi/src/database/seed/seed-employees.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ../../database/seed/seed-employees.ts
|
| 2 |
+
import { DataSource, DeepPartial } from 'typeorm';
|
| 3 |
+
import { Employee } from '@bpm/data/models/employee.entity';
|
| 4 |
+
|
| 5 |
+
export async function seedEmployees(dataSource: DataSource) {
|
| 6 |
+
const repo = dataSource.getRepository(Employee);
|
| 7 |
+
const existing = await repo.count();
|
| 8 |
+
if (existing > 0) {
|
| 9 |
+
console.log('🛑 Employees already exist. Skipping...');
|
| 10 |
+
return;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
const now = new Date();
|
| 14 |
+
const employees = repo.create([
|
| 15 |
+
{
|
| 16 |
+
FirstName: 'Nancy',
|
| 17 |
+
LastName: 'Davolio',
|
| 18 |
+
Title: 'Sales Representative',
|
| 19 |
+
TitleOfCourtesy: 'Ms.',
|
| 20 |
+
BirthDate: new Date('1948-12-08'),
|
| 21 |
+
HireDate: new Date('1992-05-01'),
|
| 22 |
+
Address: '507 - 20th Ave. E. Apt. 2A',
|
| 23 |
+
City: 'Seattle',
|
| 24 |
+
Region: 'WA',
|
| 25 |
+
PostalCode: '98122',
|
| 26 |
+
Country: 'USA',
|
| 27 |
+
HomePhone: '(206) 555-9857',
|
| 28 |
+
Extension: '5467',
|
| 29 |
+
Notes: 'Education includes a BA in psychology.',
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
FirstName: 'Andrew',
|
| 33 |
+
LastName: 'Fuller',
|
| 34 |
+
Title: 'Vice President, Sales',
|
| 35 |
+
TitleOfCourtesy: 'Dr.',
|
| 36 |
+
BirthDate: new Date('1952-02-19'),
|
| 37 |
+
HireDate: new Date('1992-08-14'),
|
| 38 |
+
Address: '908 W. Capital Way',
|
| 39 |
+
City: 'Tacoma',
|
| 40 |
+
Region: 'WA',
|
| 41 |
+
PostalCode: '98401',
|
| 42 |
+
Country: 'USA',
|
| 43 |
+
HomePhone: '(206) 555-9482',
|
| 44 |
+
Extension: '3457',
|
| 45 |
+
Notes: 'Andrew received his BTS commercial in 1974.',
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
FirstName: 'Janet',
|
| 49 |
+
LastName: 'Leverling',
|
| 50 |
+
Title: 'Sales Representative',
|
| 51 |
+
TitleOfCourtesy: 'Ms.',
|
| 52 |
+
BirthDate: new Date('1963-08-30'),
|
| 53 |
+
HireDate: new Date('1992-04-01'),
|
| 54 |
+
Address: '722 Moss Bay Blvd.',
|
| 55 |
+
City: 'Kirkland',
|
| 56 |
+
Region: 'WA',
|
| 57 |
+
PostalCode: '98033',
|
| 58 |
+
Country: 'USA',
|
| 59 |
+
HomePhone: '(206) 555-3412',
|
| 60 |
+
Extension: '3355',
|
| 61 |
+
Notes: 'Janet has a BS degree in chemistry.',
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
FirstName: 'Margaret',
|
| 65 |
+
LastName: 'Peacock',
|
| 66 |
+
Title: 'Sales Representative',
|
| 67 |
+
TitleOfCourtesy: 'Mrs.',
|
| 68 |
+
BirthDate: new Date('1937-09-19'),
|
| 69 |
+
HireDate: new Date('1993-05-03'),
|
| 70 |
+
Address: '4110 Old Redmond Rd.',
|
| 71 |
+
City: 'Redmond',
|
| 72 |
+
Region: 'WA',
|
| 73 |
+
PostalCode: '98052',
|
| 74 |
+
Country: 'USA',
|
| 75 |
+
HomePhone: '(206) 555-8122',
|
| 76 |
+
Extension: '5176',
|
| 77 |
+
Notes: 'Margaret holds a BA in English literature.',
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
FirstName: 'Steven',
|
| 81 |
+
LastName: 'Buchanan',
|
| 82 |
+
Title: 'Sales Manager',
|
| 83 |
+
TitleOfCourtesy: 'Mr.',
|
| 84 |
+
BirthDate: new Date('1955-03-04'),
|
| 85 |
+
HireDate: new Date('1993-10-17'),
|
| 86 |
+
Address: '14 Garrett Hill',
|
| 87 |
+
City: 'London',
|
| 88 |
+
Region: null,
|
| 89 |
+
PostalCode: 'SW1 8JR',
|
| 90 |
+
Country: 'UK',
|
| 91 |
+
HomePhone: '(71) 555-4848',
|
| 92 |
+
Extension: '3453',
|
| 93 |
+
Notes: 'Steven was a Navy officer before his sales career.',
|
| 94 |
+
},
|
| 95 |
+
{
|
| 96 |
+
FirstName: 'Michael',
|
| 97 |
+
LastName: 'Suyama',
|
| 98 |
+
Title: 'Sales Representative',
|
| 99 |
+
TitleOfCourtesy: 'Mr.',
|
| 100 |
+
BirthDate: new Date('1963-07-02'),
|
| 101 |
+
HireDate: new Date('1993-10-17'),
|
| 102 |
+
Address: 'Coventry House Miner Rd.',
|
| 103 |
+
City: 'London',
|
| 104 |
+
Region: null,
|
| 105 |
+
PostalCode: 'EC2 7JR',
|
| 106 |
+
Country: 'UK',
|
| 107 |
+
HomePhone: '(71) 555-7773',
|
| 108 |
+
Extension: '428',
|
| 109 |
+
Notes: 'Michael enjoys tennis and classical music.',
|
| 110 |
+
},
|
| 111 |
+
{
|
| 112 |
+
FirstName: 'Robert',
|
| 113 |
+
LastName: 'King',
|
| 114 |
+
Title: 'Sales Representative',
|
| 115 |
+
TitleOfCourtesy: 'Mr.',
|
| 116 |
+
BirthDate: new Date('1960-05-29'),
|
| 117 |
+
HireDate: new Date('1994-01-02'),
|
| 118 |
+
Address: 'Edgeham Hollow Winchester Way',
|
| 119 |
+
City: 'London',
|
| 120 |
+
Region: null,
|
| 121 |
+
PostalCode: 'RG1 9SP',
|
| 122 |
+
Country: 'UK',
|
| 123 |
+
HomePhone: '(71) 555-5598',
|
| 124 |
+
Extension: '465',
|
| 125 |
+
Notes: 'Robert is a certified Salesforce administrator.',
|
| 126 |
+
},
|
| 127 |
+
{
|
| 128 |
+
FirstName: 'Laura',
|
| 129 |
+
LastName: 'Callahan',
|
| 130 |
+
Title: 'Inside Sales Coordinator',
|
| 131 |
+
TitleOfCourtesy: 'Ms.',
|
| 132 |
+
BirthDate: new Date('1958-01-09'),
|
| 133 |
+
HireDate: new Date('1994-03-05'),
|
| 134 |
+
Address: '4726 - 11th Ave. N.E.',
|
| 135 |
+
City: 'Seattle',
|
| 136 |
+
Region: 'WA',
|
| 137 |
+
PostalCode: '98105',
|
| 138 |
+
Country: 'USA',
|
| 139 |
+
HomePhone: '(206) 555-1189',
|
| 140 |
+
Extension: '2344',
|
| 141 |
+
Notes: 'Laura is passionate about volunteer work.',
|
| 142 |
+
},
|
| 143 |
+
{
|
| 144 |
+
FirstName: 'Anne',
|
| 145 |
+
LastName: 'Dodsworth',
|
| 146 |
+
Title: 'Sales Representative',
|
| 147 |
+
TitleOfCourtesy: 'Ms.',
|
| 148 |
+
BirthDate: new Date('1966-01-27'),
|
| 149 |
+
HireDate: new Date('1994-11-15'),
|
| 150 |
+
Address: '7 Houndstooth Rd.',
|
| 151 |
+
City: 'London',
|
| 152 |
+
Region: null,
|
| 153 |
+
PostalCode: 'WG2 7LT',
|
| 154 |
+
Country: 'UK',
|
| 155 |
+
HomePhone: '(71) 555-4444',
|
| 156 |
+
Extension: '452',
|
| 157 |
+
Notes: 'Anne enjoys travel and gourmet cooking.',
|
| 158 |
+
},
|
| 159 |
+
] as DeepPartial<Employee>[]);
|
| 160 |
+
|
| 161 |
+
const savedEmployees = await repo.save(employees);
|
| 162 |
+
|
| 163 |
+
// Add ReportsTo relationships
|
| 164 |
+
await repo.save([
|
| 165 |
+
{ ...savedEmployees[0], ReportsTo: savedEmployees[1] }, // Nancy -> Andrew
|
| 166 |
+
{ ...savedEmployees[2], ReportsTo: savedEmployees[1] }, // Janet -> Andrew
|
| 167 |
+
{ ...savedEmployees[3], ReportsTo: savedEmployees[4] }, // Margaret -> Steven
|
| 168 |
+
{ ...savedEmployees[5], ReportsTo: savedEmployees[4] }, // Michael -> Steven
|
| 169 |
+
{ ...savedEmployees[6], ReportsTo: savedEmployees[4] }, // Robert -> Steven
|
| 170 |
+
{ ...savedEmployees[8], ReportsTo: savedEmployees[4] }, // Anne -> Steven
|
| 171 |
+
]);
|
| 172 |
+
console.log('✅ Seeded employees');
|
| 173 |
+
}
|
apps/northwindapi/src/database/seed/seed-order-details.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { DataSource } from 'typeorm';
|
| 2 |
+
import { OrderDetail } from '@bpm/data/models/orderdetail.entity';
|
| 3 |
+
import { Order } from '@bpm/data/models/order.entity';
|
| 4 |
+
import { Product } from '@bpm/data/models/product.entity';
|
| 5 |
+
|
| 6 |
+
export async function orderDetailSeed(dataSource: DataSource) {
|
| 7 |
+
const orderRepo = dataSource.getRepository(Order);
|
| 8 |
+
const productRepo = dataSource.getRepository(Product);
|
| 9 |
+
const orderDetailRepo = dataSource.getRepository(OrderDetail);
|
| 10 |
+
|
| 11 |
+
const existing = await orderDetailRepo.count();
|
| 12 |
+
if (existing > 0) {
|
| 13 |
+
console.log('🛑 OrderDetail already exist. Skipping...');
|
| 14 |
+
return;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
const orders = await orderRepo.find();
|
| 18 |
+
const products = await productRepo.find();
|
| 19 |
+
|
| 20 |
+
if (orders.length === 0 || products.length === 0) {
|
| 21 |
+
throw new Error(
|
| 22 |
+
'🛑 Orders or Products must be seeded before OrderDetails.',
|
| 23 |
+
);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
// const orders = await orderRepo.find({ take: 10 }); // adjust as needed
|
| 27 |
+
// const products = await productRepo.find({ take: 30 });
|
| 28 |
+
|
| 29 |
+
const orderDetails: OrderDetail[] = [];
|
| 30 |
+
|
| 31 |
+
for (let i = 0; i < 20; i++) {
|
| 32 |
+
const order = orders[Math.floor(Math.random() * orders.length)];
|
| 33 |
+
const product = products[Math.floor(Math.random() * products.length)];
|
| 34 |
+
|
| 35 |
+
const detail = new OrderDetail();
|
| 36 |
+
detail.Order = order;
|
| 37 |
+
detail.OrderID = order.OrderID;
|
| 38 |
+
detail.Product = product;
|
| 39 |
+
detail.ProductID = product.ProductID;
|
| 40 |
+
detail.UnitPrice = parseFloat((Math.random() * 100 + 10).toFixed(2));
|
| 41 |
+
detail.Quantity = Math.floor(Math.random() * 10) + 1;
|
| 42 |
+
detail.Discount = parseFloat((Math.random() * 0.5).toFixed(2));
|
| 43 |
+
|
| 44 |
+
orderDetails.push(detail);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
await orderDetailRepo.save(orderDetails);
|
| 48 |
+
console.log('✅ Seeded OrderDetails with realistic product lines.');
|
| 49 |
+
}
|