96 switch from azure table storage to sqlite (#97)

* adding typeorm to api

* switching to sqlite

* switching to sqlite

* switching to sqlite

* migrating to sqlite

* updating terraform

* updating infrastructure
This commit was merged in pull request #97.
This commit is contained in:
2025-11-23 10:42:31 -06:00
committed by GitHub
parent f94d0f7ca9
commit f98a2ab127
208 changed files with 27135 additions and 16875 deletions

3
api/.gitignore vendored
View File

@@ -57,3 +57,6 @@ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
local.settings.json
/src/database/*.db

3
api/entrypoint.sh Normal file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
npx typeorm migration:run -d ./app/api/dist/database/data-source.js
node ./app/api/dist/main.js

View File

@@ -1,6 +1,6 @@
{
"name": "api",
"version": "1.4.0",
"version": "2.0.0-alpha",
"description": "",
"author": "",
"private": true,
@@ -16,24 +16,40 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
"test:e2e": "jest --config ./test/jest-e2e.json",
"typeorm": "npm run build && npx typeorm -d dist/database/data-source.js",
"migration:generate": "npm run typeorm -- migration:generate",
"migration:run": "npm run typeorm -- migration:run",
"migration:revert": "npm run typeorm -- migration:revert"
},
"dependencies": {
"@azure/storage-blob": "^12.27.0",
"@microsoft/microsoft-graph-client": "^3.0.7",
"@nestjs/axios": "^3.0.3",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.2",
"@nestjs/core": "^10.0.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/axios": "^4.0.1",
"@nestjs/common": "^11.1.6",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.1.6",
"@nestjs/jwt": "^11.0.0",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.1.6",
"@nestjs/serve-static": "^5.0.3",
"@nestjs/typeorm": "^11.0.0",
"@noahspan/azure-database": "^3.1.2",
"@noahspan/noahspan-modules": "^1.1.5",
"@noahspan/noahspan-modules": "^1.2.9",
"@schematics/angular": "^17.3.7",
"@types/multer": "^1.4.12",
"dotenv": "^16.4.7",
"better-sqlite3": "^12.2.0",
"dotenv": "^16.6.1",
"express-session": "^1.18.2",
"jwks-rsa": "^3.2.0",
"jwt-decode": "^4.0.0",
"node-gyp": "^11.4.1",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-openidconnect": "^0.1.2",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"typeorm": "^0.3.25",
"uuid": "^10.0.0",
"uuidv4": "^6.2.13"
},
@@ -41,11 +57,13 @@
"@microsoft/microsoft-graph-types": "^2.40.0",
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@nestjs/testing": "^11.1.6",
"@types/express": "^4.17.17",
"@types/express-session": "^1.18.2",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/passport-azure-ad": "^4.3.6",
"@types/passport-openidconnect": "^0.1.3",
"@types/supertest": "^6.0.0",
"jest": "^29.5.0",
"source-map-support": "^0.5.21",

View File

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

View File

@@ -1,12 +1,17 @@
import { Module } from '@nestjs/common';
import { FeatureFlagModule } from './featureFlag/feature-flag.module'
import { HealthModule } from './health/health.module';
import { LogModule } from './log/log.module';
import { PilotModule } from './pilot/pilot.module';
import { APP_FILTER, APP_GUARD, Reflector } from '@nestjs/core';
import { HttpExceptionFilter } from './filters/http-exception.filter';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthGuard, AuthModule, UserModule } from '@noahspan/noahspan-modules';
import { AuthModule, MsGraphModule } from '@noahspan/noahspan-modules';
import configuration from './config/configuration';
import { TypeOrmModule } from '@nestjs/typeorm';
import { dataSourceOptions } from './database/data-source';
import { TrackModule } from './track/track.module';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';
@Module({
imports: [
@@ -15,19 +20,25 @@ import configuration from './config/configuration';
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => {
return {
clientId: configService.get<string>('clientId'),
clientSecret: configService.get<string>('clientSecret'),
tenantId: configService.get<string>('tenantId')
audience: configService.get<string>('audience'),
issuerUrl: configService.get<string>('issuer'),
jwksUri: configService.get<string>('jwksUri')
};
},
}),
ConfigModule.forRoot({
isGlobal: true,
load: [configuration]
}),
FeatureFlagModule,
// HealthModule,
LogModule,
PilotModule,
UserModule.registerAsync({
ServeStaticModule.forRoot({
rootPath: join(__dirname, '../..', 'client', 'dist')
}),
TrackModule,
TypeOrmModule.forRoot(dataSourceOptions),
MsGraphModule.registerAsync({
inject: [ConfigService],
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => {
@@ -43,7 +54,7 @@ import configuration from './config/configuration';
{
provide: APP_FILTER,
useClass: HttpExceptionFilter
},
}
]
})
export class AppModule {}

View File

@@ -1,5 +0,0 @@
export interface AuthModuleOptions {
tenantId: string;
clientId: string;
clientSecret: string;
}

View File

@@ -1,4 +0,0 @@
import { ConfigurableModuleBuilder } from '@nestjs/common';
import { AuthModuleOptions } from './auth.interface';
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = new ConfigurableModuleBuilder<AuthModuleOptions>().build()

View File

@@ -1,14 +0,0 @@
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { AzureAdStrategy } from './auth.strategy';
import { ConfigurableModuleClass } from './auth.module-definition';
@Module({
imports: [
PassportModule.register({
defaultStrategy: 'azure-ad'
})
],
providers: [AzureAdStrategy]
})
export class AuthModule extends ConfigurableModuleClass {}

View File

@@ -1,25 +0,0 @@
import { Inject, Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { AuthModuleOptions } from './auth.interface'
import { MODULE_OPTIONS_TOKEN } from "./auth.module-definition";
import { BearerStrategy } from 'passport-azure-ad'
@Injectable()
export class AzureAdStrategy extends PassportStrategy(
BearerStrategy,
'azure-ad'
) {
constructor(@Inject(MODULE_OPTIONS_TOKEN) authModuleOptions: AuthModuleOptions) {
super({
identityMetadata: `https://login.microsoftonline.com/${authModuleOptions.tenantId}/.well-known/openid-configuration`,
clientID: authModuleOptions.clientId,
audience: `api://${authModuleOptions.clientId}`,
loggingLevel: 'info',
loggingNoPII: false
})
}
async validate(data: any): Promise<any> {
return data;
}
}

View File

View File

@@ -0,0 +1,21 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { PilotEntity } from '../pilot/pilot.entity';
@Entity({ name: 'certificates'})
export class CertificateEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
type: string;
@Column()
number: string;
@Column()
issueDate: Date
@ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.certificates, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
@JoinColumn({ name: 'pilotId' })
pilot: PilotEntity;
}

View File

@@ -1,6 +1,9 @@
export default () => ({
azureStorageConnectionString: process.env.AZURE_STORAGE_CONNECTION_STRING,
audience: process.env.AUDIENCE,
clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
issuer: process.env.ISSUER_URL,
jwksUri: process.env.JWKS_URI,
tenantId: process.env.TENANT_ID
})

View File

@@ -0,0 +1,19 @@
import { DataSource, DataSourceOptions } from 'typeorm';
import { config } from 'dotenv';
import { ConfigService } from '@nestjs/config';
config();
const configService = new ConfigService();
export const dataSourceOptions: DataSourceOptions = {
type: 'better-sqlite3',
database: configService.get<string>('DB_PATH'),
entities: ['../**/*.entity.js'],
migrations: ['./migrations/*.js'],
synchronize: configService.get<boolean>('DB_SYNC')
}
const dataSource = new DataSource(dataSourceOptions);
export default dataSource;

View File

@@ -0,0 +1,4 @@
dbs:
- path: /var/lib/data/flying.db
replicas:
- path: /mnt/data/backup

View File

@@ -0,0 +1,64 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class InitialMigration1758802917932 implements MigrationInterface {
name = 'InitialMigration1758802917932'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "certificates" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar NOT NULL, "number" varchar NOT NULL, "issueDate" datetime NOT NULL, "pilotId" varchar)`);
await queryRunner.query(`CREATE TABLE "endorsements" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar NOT NULL, "issueDate" datetime NOT NULL, "pilotId" varchar)`);
await queryRunner.query(`CREATE TABLE "medical" ("id" varchar PRIMARY KEY NOT NULL, "class" varchar NOT NULL, "expirationDate" datetime NOT NULL, "pilotId" varchar)`);
await queryRunner.query(`CREATE TABLE "pilots" ("id" varchar PRIMARY KEY NOT NULL, "name" varchar NOT NULL, "address" varchar NOT NULL, "city" varchar NOT NULL, "state" varchar NOT NULL, "postalCode" varchar NOT NULL, "email" varchar NOT NULL, "phone" varchar NOT NULL, "userId" varchar NOT NULL)`);
await queryRunner.query(`CREATE TABLE "logs" ("id" varchar PRIMARY KEY NOT NULL, "date" datetime NOT NULL, "aircraftMakeModel" varchar NOT NULL, "aircraftIdentity" varchar NOT NULL, "routeFrom" varchar NOT NULL, "routeTo" varchar NOT NULL, "durationOfFlight" integer NOT NULL, "singleEngineLand" integer, "simulatorAtd" integer, "landingsDay" integer, "landingsNight" integer, "groundTrainingReceived" integer, "flightTrainingReceived" integer, "crossCountry" integer, "night" integer, "solo" integer, "pilotInCommand" integer, "instrumentActual" integer, "instrumentSimulated" integer, "instrumentApproaches" integer, "instrumentHolds" integer, "instrumentNavTrack" integer, "notes" varchar, "pilotId" varchar)`);
await queryRunner.query(`CREATE TABLE "tracks" ("id" varchar PRIMARY KEY NOT NULL, "url" varchar NOT NULL, "order" integer NOT NULL, "logId" varchar)`);
await queryRunner.query(`CREATE TABLE "temporary_certificates" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar NOT NULL, "number" varchar NOT NULL, "issueDate" datetime NOT NULL, "pilotId" varchar, CONSTRAINT "FK_05a68997dc2d27dfcc642a4cf51" FOREIGN KEY ("pilotId") REFERENCES "pilots" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`INSERT INTO "temporary_certificates"("id", "type", "number", "issueDate", "pilotId") SELECT "id", "type", "number", "issueDate", "pilotId" FROM "certificates"`);
await queryRunner.query(`DROP TABLE "certificates"`);
await queryRunner.query(`ALTER TABLE "temporary_certificates" RENAME TO "certificates"`);
await queryRunner.query(`CREATE TABLE "temporary_endorsements" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar NOT NULL, "issueDate" datetime NOT NULL, "pilotId" varchar, CONSTRAINT "FK_96a69a0bfbdeccce6bffe34002d" FOREIGN KEY ("pilotId") REFERENCES "pilots" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`INSERT INTO "temporary_endorsements"("id", "type", "issueDate", "pilotId") SELECT "id", "type", "issueDate", "pilotId" FROM "endorsements"`);
await queryRunner.query(`DROP TABLE "endorsements"`);
await queryRunner.query(`ALTER TABLE "temporary_endorsements" RENAME TO "endorsements"`);
await queryRunner.query(`CREATE TABLE "temporary_medical" ("id" varchar PRIMARY KEY NOT NULL, "class" varchar NOT NULL, "expirationDate" datetime NOT NULL, "pilotId" varchar, CONSTRAINT "FK_cb1f5d88fa2b105cc77513e9082" FOREIGN KEY ("pilotId") REFERENCES "pilots" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`INSERT INTO "temporary_medical"("id", "class", "expirationDate", "pilotId") SELECT "id", "class", "expirationDate", "pilotId" FROM "medical"`);
await queryRunner.query(`DROP TABLE "medical"`);
await queryRunner.query(`ALTER TABLE "temporary_medical" RENAME TO "medical"`);
await queryRunner.query(`CREATE TABLE "temporary_logs" ("id" varchar PRIMARY KEY NOT NULL, "date" datetime NOT NULL, "aircraftMakeModel" varchar NOT NULL, "aircraftIdentity" varchar NOT NULL, "routeFrom" varchar NOT NULL, "routeTo" varchar NOT NULL, "durationOfFlight" integer NOT NULL, "singleEngineLand" integer, "simulatorAtd" integer, "landingsDay" integer, "landingsNight" integer, "groundTrainingReceived" integer, "flightTrainingReceived" integer, "crossCountry" integer, "night" integer, "solo" integer, "pilotInCommand" integer, "instrumentActual" integer, "instrumentSimulated" integer, "instrumentApproaches" integer, "instrumentHolds" integer, "instrumentNavTrack" integer, "notes" varchar, "pilotId" varchar, CONSTRAINT "FK_19598551658f7625a82c7f029c8" FOREIGN KEY ("pilotId") REFERENCES "pilots" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`INSERT INTO "temporary_logs"("id", "date", "aircraftMakeModel", "aircraftIdentity", "routeFrom", "routeTo", "durationOfFlight", "singleEngineLand", "simulatorAtd", "landingsDay", "landingsNight", "groundTrainingReceived", "flightTrainingReceived", "crossCountry", "night", "solo", "pilotInCommand", "instrumentActual", "instrumentSimulated", "instrumentApproaches", "instrumentHolds", "instrumentNavTrack", "notes", "pilotId") SELECT "id", "date", "aircraftMakeModel", "aircraftIdentity", "routeFrom", "routeTo", "durationOfFlight", "singleEngineLand", "simulatorAtd", "landingsDay", "landingsNight", "groundTrainingReceived", "flightTrainingReceived", "crossCountry", "night", "solo", "pilotInCommand", "instrumentActual", "instrumentSimulated", "instrumentApproaches", "instrumentHolds", "instrumentNavTrack", "notes", "pilotId" FROM "logs"`);
await queryRunner.query(`DROP TABLE "logs"`);
await queryRunner.query(`ALTER TABLE "temporary_logs" RENAME TO "logs"`);
await queryRunner.query(`CREATE TABLE "temporary_tracks" ("id" varchar PRIMARY KEY NOT NULL, "url" varchar NOT NULL, "order" integer NOT NULL, "logId" varchar, CONSTRAINT "FK_71881df31cfff2362e39accc4b2" FOREIGN KEY ("logId") REFERENCES "logs" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`INSERT INTO "temporary_tracks"("id", "url", "order", "logId") SELECT "id", "url", "order", "logId" FROM "tracks"`);
await queryRunner.query(`DROP TABLE "tracks"`);
await queryRunner.query(`ALTER TABLE "temporary_tracks" RENAME TO "tracks"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "tracks" RENAME TO "temporary_tracks"`);
await queryRunner.query(`CREATE TABLE "tracks" ("id" varchar PRIMARY KEY NOT NULL, "url" varchar NOT NULL, "order" integer NOT NULL, "logId" varchar)`);
await queryRunner.query(`INSERT INTO "tracks"("id", "url", "order", "logId") SELECT "id", "url", "order", "logId" FROM "temporary_tracks"`);
await queryRunner.query(`DROP TABLE "temporary_tracks"`);
await queryRunner.query(`ALTER TABLE "logs" RENAME TO "temporary_logs"`);
await queryRunner.query(`CREATE TABLE "logs" ("id" varchar PRIMARY KEY NOT NULL, "date" datetime NOT NULL, "aircraftMakeModel" varchar NOT NULL, "aircraftIdentity" varchar NOT NULL, "routeFrom" varchar NOT NULL, "routeTo" varchar NOT NULL, "durationOfFlight" integer NOT NULL, "singleEngineLand" integer, "simulatorAtd" integer, "landingsDay" integer, "landingsNight" integer, "groundTrainingReceived" integer, "flightTrainingReceived" integer, "crossCountry" integer, "night" integer, "solo" integer, "pilotInCommand" integer, "instrumentActual" integer, "instrumentSimulated" integer, "instrumentApproaches" integer, "instrumentHolds" integer, "instrumentNavTrack" integer, "notes" varchar, "pilotId" varchar)`);
await queryRunner.query(`INSERT INTO "logs"("id", "date", "aircraftMakeModel", "aircraftIdentity", "routeFrom", "routeTo", "durationOfFlight", "singleEngineLand", "simulatorAtd", "landingsDay", "landingsNight", "groundTrainingReceived", "flightTrainingReceived", "crossCountry", "night", "solo", "pilotInCommand", "instrumentActual", "instrumentSimulated", "instrumentApproaches", "instrumentHolds", "instrumentNavTrack", "notes", "pilotId") SELECT "id", "date", "aircraftMakeModel", "aircraftIdentity", "routeFrom", "routeTo", "durationOfFlight", "singleEngineLand", "simulatorAtd", "landingsDay", "landingsNight", "groundTrainingReceived", "flightTrainingReceived", "crossCountry", "night", "solo", "pilotInCommand", "instrumentActual", "instrumentSimulated", "instrumentApproaches", "instrumentHolds", "instrumentNavTrack", "notes", "pilotId" FROM "temporary_logs"`);
await queryRunner.query(`DROP TABLE "temporary_logs"`);
await queryRunner.query(`ALTER TABLE "medical" RENAME TO "temporary_medical"`);
await queryRunner.query(`CREATE TABLE "medical" ("id" varchar PRIMARY KEY NOT NULL, "class" varchar NOT NULL, "expirationDate" datetime NOT NULL, "pilotId" varchar)`);
await queryRunner.query(`INSERT INTO "medical"("id", "class", "expirationDate", "pilotId") SELECT "id", "class", "expirationDate", "pilotId" FROM "temporary_medical"`);
await queryRunner.query(`DROP TABLE "temporary_medical"`);
await queryRunner.query(`ALTER TABLE "endorsements" RENAME TO "temporary_endorsements"`);
await queryRunner.query(`CREATE TABLE "endorsements" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar NOT NULL, "issueDate" datetime NOT NULL, "pilotId" varchar)`);
await queryRunner.query(`INSERT INTO "endorsements"("id", "type", "issueDate", "pilotId") SELECT "id", "type", "issueDate", "pilotId" FROM "temporary_endorsements"`);
await queryRunner.query(`DROP TABLE "temporary_endorsements"`);
await queryRunner.query(`ALTER TABLE "certificates" RENAME TO "temporary_certificates"`);
await queryRunner.query(`CREATE TABLE "certificates" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar NOT NULL, "number" varchar NOT NULL, "issueDate" datetime NOT NULL, "pilotId" varchar)`);
await queryRunner.query(`INSERT INTO "certificates"("id", "type", "number", "issueDate", "pilotId") SELECT "id", "type", "number", "issueDate", "pilotId" FROM "temporary_certificates"`);
await queryRunner.query(`DROP TABLE "temporary_certificates"`);
await queryRunner.query(`DROP TABLE "tracks"`);
await queryRunner.query(`DROP TABLE "logs"`);
await queryRunner.query(`DROP TABLE "pilots"`);
await queryRunner.query(`DROP TABLE "medical"`);
await queryRunner.query(`DROP TABLE "endorsements"`);
await queryRunner.query(`DROP TABLE "certificates"`);
}
}

View File

@@ -0,0 +1,65 @@
import { Body, Controller, Delete, Get, HttpException, Param, Post, Put } from "@nestjs/common";
import { EndorsementService } from "./endorsement.service";
import { CustomError } from "@noahspan/noahspan-modules";
import { EndorsementDto } from "./endorsement.dto";
@Controller('endorsements')
export class EndorsementController {
constructor(private readonly endorsementService: EndorsementService) {}
@Get(':id')
async find(@Param('id') id: string) {
try {
return await this.endorsementService.find(id);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
@Get()
async findAll() {
try {
return await this.endorsementService.findAll();
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
@Post()
async create(@Body() endorsementDto: EndorsementDto) {
try {
return await this.endorsementService.create(endorsementDto);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
@Put(':id')
async update(@Param('id') id: string, @Body() endorsementDto: EndorsementDto) {
try {
return await this.endorsementService.update(id, endorsementDto);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode)
}
}
@Delete(':id')
async delete(@Param('id') id: string) {
try {
return await this.endorsementService.delete(id);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode)
}
}
}

View File

@@ -0,0 +1,3 @@
export class EndorsementDto {
}

View File

@@ -0,0 +1,18 @@
import { PilotEntity } from 'src/pilot/pilot.entity';
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
@Entity({ name: 'endorsements' })
export class EndorsementEntity {
@PrimaryGeneratedColumn('uuid')
id: string
@Column()
type: string;
@Column()
issueDate: Date;
@ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.endorsements, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
@JoinColumn({ name: 'pilotId' })
pilot: PilotEntity
}

View File

@@ -0,0 +1,32 @@
import { InjectRepository } from "@nestjs/typeorm";
import { EndorsementEntity } from "./endorsement.entity";
import { DeleteResult, InsertResult, Repository, UpdateResult } from 'typeorm';
import { Injectable } from "@nestjs/common";
import { EndorsementDto } from "./endorsement.dto";
@Injectable()
export class EndorsementService {
constructor(
@InjectRepository(EndorsementEntity) private readonly endorsementRepository: Repository<EndorsementEntity>
) {}
async find(id: string): Promise<EndorsementEntity> {
return await this.endorsementRepository.findOneBy({ id });
}
async findAll(): Promise<EndorsementEntity[]> {
return await this.endorsementRepository.find();
}
async create(endorsement: EndorsementDto): Promise<InsertResult> {
return await this.endorsementRepository.insert(endorsement);
}
async update(id: string, endorsement: EndorsementDto): Promise<UpdateResult> {
return await this.endorsementRepository.update(id, endorsement);
}
async delete(id: string): Promise<DeleteResult> {
return await this.endorsementRepository.delete({ id });
}
}

View File

@@ -1,41 +0,0 @@
import {
Controller,
Get,
HttpException,
Param,
UseGuards,
} from '@nestjs/common';
import { FeatureFlagService } from './feature-flag.service';
import { CustomError } from '../error/customError';
import { AuthGuard } from '@nestjs/passport';
@Controller('featureFlags')
@UseGuards(AuthGuard('azure-ad'))
export class FeatureFlagController {
constructor(private readonly featureFlagService: FeatureFlagService) {}
@Get(':partitionKey/:rowKey')
async find(
@Param('partitionKey') partitionKey: string,
@Param('rowKey') rowKey: string
) {
try {
return await this.featureFlagService.find(partitionKey, rowKey);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
@Get()
async findAll() {
try {
return await this.featureFlagService.findAll();
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
}

View File

@@ -1,5 +0,0 @@
export class FeatureFlagDto {
partitionKey: string;
rowKey: string;
active: string;
}

View File

@@ -1,7 +0,0 @@
import { EntityString } from '@noahspan/azure-database';
export class FeatureFlag {
@EntityString() partitionKey: string;
@EntityString() rowKey: string;
@EntityString() active: string;
}

View File

@@ -1,27 +0,0 @@
import { Module } from '@nestjs/common';
import { FeatureFlagController } from './feature-flag.controller';
import { FeatureFlagService } from './feature-flag.service';
import { AzureTableStorageModule } from '@noahspan/azure-database';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { FeatureFlag } from './feature-flag.entity';
@Module({
imports: [
AzureTableStorageModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => {
return {
connectionString: configService.get<string>('azureStorageConnectionString')
};
},
inject: [ConfigService]
}),
AzureTableStorageModule.forFeature(FeatureFlag, {
createTableIfNotExists: false,
table: 'featureFlags'
}),
],
controllers: [FeatureFlagController],
providers: [FeatureFlagService]
})
export class FeatureFlagModule {}

View File

@@ -1,18 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository, Repository } from '@noahspan/azure-database';
import { FeatureFlag } from './feature-flag.entity';
@Injectable()
export class FeatureFlagService {
constructor(
@InjectRepository(FeatureFlag) private readonly featureFlagRepository: Repository<FeatureFlag>
) {}
async find(partitionKey: string, rowKey: string): Promise<FeatureFlag> {
return await this.featureFlagRepository.find(partitionKey, rowKey);
}
async findAll(): Promise<FeatureFlag[]> {
return await this.featureFlagRepository.findAll();
}
}

View File

@@ -37,10 +37,10 @@ import { ConfigService } from '@nestjs/config';
return blockBlobClient;
}
async uploadFile(file: Express.Multer.File, containerName: string, rowKey: string): Promise<string> {
async uploadFile(file: Express.Multer.File, containerName: string, logId: string): Promise<string> {
this.containerName = containerName;
const blockBlobClient = await this.getBlobClient(`${rowKey}/${file.originalname}`);
const blockBlobClient = await this.getBlobClient(`${logId}/${file.originalname}`);
const fileUrl = blockBlobClient.url;
await blockBlobClient.uploadData(file.buffer);
@@ -58,11 +58,32 @@ import { ConfigService } from '@nestjs/config';
return downloaded
}
async deleteFile(containerName: string, rowKey:string, fileName: string): Promise<void> {
async deleteFile(containerName: string, logId: string, fileName: string): Promise<void> {
this.containerName = containerName;
const blockBlobClient = await this.getBlobClient(`${rowKey}/${fileName}`);
const blockBlobClient = await this.getBlobClient(`${logId}/${fileName}`);
await blockBlobClient.deleteIfExists();
}
async deleteFolder(containerName: string, logId: string): Promise<void> {
const blobService = await this.getBlobServiceInstance();
this.containerName = containerName;
const containerClient = blobService.getContainerClient(containerName);
const blobsToDelete = []
for await (const blob of containerClient.listBlobsFlat({ prefix: logId })) {
blobsToDelete.push(blob.name)
}
for (const blobName of blobsToDelete) {
const blockBlobClient = containerClient.getBlockBlobClient(blobName);
await blockBlobClient.delete();
}
return;
}
}

View File

@@ -0,0 +1,42 @@
import { Test, TestingModule } from '@nestjs/testing';
import { HealthController } from './health.controller';
import { HealthService } from './health.service';
describe('HealthController', () => {
let controller; HealthController;
const mockHealthService = {
isDatabaseConnected: jest.fn()
}
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [HealthController],
providers: [HealthService]
}).compile();
controller = module.get<HealthController>(HealthController);
})
it('isHealthy => should return true', () => {
expect(controller).toBeDefined();
});
it('should return database connected', async () => {
jest.spyOn(mockHealthService, 'isDatabaseConnected').mockReturnValue(true);
const result = await controller.isHealthy();
expect(mockHealthService.isDatabaseConnected).toHaveBeenCalled();
expect(result).toEqual(true);
})
it('isHealthy => should return error', async () => {
jest.spyOn(mockHealthService, 'isDatabaseConnected').mockReturnValue(false);
const result = await controller.isHealthy();
expect(mockHealthService.isDatabaseConnected).toHaveBeenCalled();
expect(result).toEqual(false);
})
})

View File

@@ -0,0 +1,27 @@
import {
Controller,
Get,
HttpException,
} from '@nestjs/common';
import { HealthService } from './health.service';
import { CustomError } from 'src/error/customError';
@Controller('health')
export class HealthController {
constructor(
private readonly healthService: HealthService
) {}
@Get()
async isHealthy(): Promise<boolean> {
try {
return await this.healthService.isDatabaseConnected();
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
}

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { HealthController } from './health.controller';
import { HealthService } from './health.service';
@Module({
controllers: [HealthController],
providers: [
HealthService
]
})
export class HealthModule {}

View File

@@ -0,0 +1,14 @@
import { Test, TestingModule } from '@nestjs/testing';
import { HealthService } from './health.service';
describe('HealthService', () => {
let service: HealthService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [HealthService]
}).compile();
service = module.get<HealthService>(HealthService);
});
});

View File

@@ -0,0 +1,22 @@
import { Injectable } from '@nestjs/common';
import { CustomError } from '../error/customError';
import { DataSource } from 'typeorm';
@Injectable()
export class HealthService {
constructor(private dataSource: DataSource) {}
async isDatabaseConnected(): Promise<boolean> {
try {
const isDatabaseConnected: boolean = this.dataSource.isInitialized;
if (isDatabaseConnected) {
return isDatabaseConnected;
} else {
throw new CustomError('Database not connected', 'Database not connected', 400)
}
} catch (error) {
throw error;
}
}
}

View File

@@ -0,0 +1,5 @@
import { JwtPayload } from "jwt-decode";
export interface CustomJwtPayload extends JwtPayload {
roles: string[];
}

View File

@@ -1,41 +0,0 @@
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
import { Observable, map } from 'rxjs';
export class LogInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1];
console.log(token)
if (!token) {
return handler.handle().pipe(
map((data) => {
if (data.length) {
const logs = data.map((log) => {
return {
partitionKey: log.partitionKey,
rowKey: log.rowKey,
pilotId: log.pilotId,
pilotName: log.pilotName,
date: log.date,
aircraftMakeModel: log.aircraftMakeModel,
routeFrom: log.routeFrom,
routeTo: log.routeTo,
durationOfFlight: log.durationOfFlight,
tracks: log.tracks,
notes: log.notes
};
});
return logs;
} else {
return data;
}
})
);
}
return handler.handle().pipe(map((data) => data));
}
}

View File

@@ -7,22 +7,23 @@ import {
Param,
Post,
Put,
Query,
StreamableFile,
UploadedFile,
UseGuards,
UseInterceptors
} from '@nestjs/common';
import { LogDto } from './log.dto';
import { Log } from './log.entity';
import { LogEntity } from './log.entity';
import { LogService } from './log.service';
import { CustomError } from '../error/customError';
import { AuthGuard } from '@noahspan/noahspan-modules';
import { LogInterceptor } from './interceptors/log.interceptor';
import { AuthGuard, Public } from '@noahspan/noahspan-modules';
import { LogInterceptor } from './log.interceptor';
import { FileService } from '../file/file.service';
import { FileInterceptor } from '@nestjs/platform-express';
import { DeleteResult, InsertResult, UpdateResult } from 'typeorm';
import { Reflector } from '@nestjs/core';
const reflector = new Reflector();
@Controller('logs')
@UseInterceptors(new LogInterceptor(reflector))
export class LogController {
constructor(
private readonly fileService: FileService,
@@ -30,116 +31,73 @@ export class LogController {
) {}
@Get(':partitionKey/:rowKey')
@UseInterceptors(new LogInterceptor())
@Get(':id')
@Public()
async find(
@Param('partitionKey') partitionKey: string,
@Param('rowKey') rowKey: string
): Promise<Log> {
@Param('id') id: string,
): Promise<LogEntity> {
try {
return await this.logService.find(partitionKey, rowKey);
console.log(id)
return await this.logService.find(id);
} catch (error) {
const customError = error as CustomError;
console.log(error)
throw new HttpException(customError.message, customError.statusCode);
}
}
@Get()
@UseInterceptors(new LogInterceptor())
async findAll(): Promise<Log[]> {
@Public()
async findAll(): Promise<LogEntity[]> {
try {
console.log('blah')
return await this.logService.findAll();
} catch (error) {
const customError = error as CustomError;
console.log(error)
throw new HttpException(customError.message, customError.statusCode);
}
}
@UseGuards(AuthGuard)
@Post()
async create(@Body() logDto: LogDto): Promise<Log> {
try {
const log = new Log();
Object.assign(log, logDto);
return await this.logService.create(log);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
@UseGuards(AuthGuard)
@Put(':partitionKey/:rowKey')
async update(
@Param('partitionKey') partitionKey: string,
@Param('rowKey') rowKey: string,
@Body() logDto: LogDto
): Promise<Log> {
try {
const log = new Log();
Object.assign(log, logDto);
return await this.logService.update(partitionKey, rowKey, log);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
@UseGuards(AuthGuard)
@Delete(':partitionKey/:rowKey')
async delete(
@Param('partitionKey') partitionKey: string,
@Param('rowKey') rowKey: string
): Promise<void> {
try {
return await this.logService.delete(partitionKey, rowKey);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
@UseGuards(AuthGuard)
@Post(':partitionKey/:rowKey/track')
@UseInterceptors(FileInterceptor('file'))
async createTrack(@Param('rowKey') rowKey: string, @UploadedFile() file: Express.Multer.File) {
try {
const containerName = 'tracks';
const url = await this.fileService.uploadFile(file, containerName, rowKey);
return { url }
@Post()
@UseGuards(AuthGuard)
async create(@Body() logDto: LogDto): Promise<InsertResult> {
try {
return await this.logService.create(logDto);
} catch (error) {
const customError = error as CustomError;
console.log(error)
throw new HttpException(customError.message, customError.statusCode);
}
}
@Get(':partitionKey/:rowKey/track')
async downloadTrack(@Param('rowKey') rowKey: string, @Query('fileName') fileName: string): Promise<string> {
const containerName = 'tracks';
const downloadedFile: string = await this.fileService.downloadFile(containerName, rowKey, fileName)
return downloadedFile;
@Put(':id')
@UseGuards(AuthGuard)
async update(
@Param('id') id: string,
@Body() logDto: LogDto
): Promise<UpdateResult> {
try {
console.log(id)
console.log(logDto)
return await this.logService.update(id, logDto);
} catch (error) {
const customError = error as CustomError;
console.log(error)
throw new HttpException(customError.message, customError.statusCode);
}
}
@Delete(':id')
@UseGuards(AuthGuard)
@Delete(':partitionKey/:rowKey/track')
async deleteTrack(@Param('rowKey') rowKey: string, @Query('fileName') fileName: string): Promise<void> {
async delete(
@Param('id') id: string,
): Promise<DeleteResult> {
try {
const containerName = 'tracks';
return await this.fileService.deleteFile(containerName, rowKey, fileName)
return await this.logService.delete(id);
} catch (error) {
console.log(error)
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);

View File

@@ -1,27 +1,28 @@
import { PilotEntity } from "src/pilot/pilot.entity";
export class LogDto {
pilotId: string;
pilotName: string;
date: string;
date: Date;
aircraftMakeModel: string;
aircraftIdentity: string;
routeFrom: string;
routeTo: string;
durationOfFlight: number;
singleEngineLand: string;
simulatorAtd: number;
landingsDay: number;
landingsNight: number;
instrumentActual: number;
instrumentSimulated: number;
instrumentApproaches: number;
instrumentHolds: number;
instrumentNavTrack: number;
groundTrainingReceived: number;
flightTrainingReceived: number;
crossCountry: number;
night: number;
solo: number;
pilotInCommand: number;
tracks: string[];
notes: string;
durationOfFlight?: number;
singleEngineLand?: number;
simulatorAtd?: number;
landingsDay?: number;
landingsNight?: number;
instrumentActual?: number;
instrumentSimulated?: number;
instrumentApproaches?: number;
instrumentHolds?: number;
instrumentNavTrack?: number;
groundTrainingReceived?: number;
flightTrainingReceived?: number;
crossCountry?: number;
night?: number;
solo?: number;
pilotInCommand?: number;
notes?: string;
tracks?: []
}

View File

@@ -1,29 +1,85 @@
export class Log {
partitionKey: string;
rowKey: string;
import { PilotEntity } from 'src/pilot/pilot.entity';
import { TrackEntity } from 'src/track/track.entity';
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, OneToMany } from 'typeorm';
@Entity({ name: 'logs' })
export class LogEntity {
@PrimaryGeneratedColumn('uuid')
id: string
@Column()
pilotId: string;
pilotName: string;
date: string;
@Column()
date: Date;
@Column()
aircraftMakeModel: string;
@Column()
aircraftIdentity: string;
@Column()
routeFrom: string;
@Column()
routeTo: string;
durationOfFlight: number | null;
@Column()
durationOfFlight: number;
@Column({ nullable: true })
singleEngineLand: number | null;
simulatorAtd?: number | null;
landingsDay?: number | null;
landingsNight?: number | null;
groundTrainingReceived?: number;
flightTrainingReceived?: number;
crossCountry?: number | null;
night?: number | null;
solo?: number | null;
pilotInCommand?: number | null;
instrumentActual?: number | null;
instrumentSimulated?: number | null;
instrumentApproaches?: number | null;
instrumentHolds?: number | null;
instrumentNavTrack?: number | null;
tracks?: string[];
notes?: string;
}
@Column({ nullable: true })
simulatorAtd: number | null;
@Column({ nullable: true })
landingsDay: number | null;
@Column({ nullable: true })
landingsNight: number | null;
@Column({ nullable: true })
groundTrainingReceived: number | null;
@Column({ nullable: true })
flightTrainingReceived: number | null;
@Column({ nullable: true })
crossCountry: number | null;
@Column({ nullable: true })
night: number | null;
@Column({ nullable: true })
solo: number | null;
@Column({ nullable: true })
pilotInCommand: number | null;
@Column({ nullable: true })
instrumentActual: number | null;
@Column({ nullable: true })
instrumentSimulated: number | null;
@Column({ nullable: true })
instrumentApproaches: number | null;
@Column({ nullable: true })
instrumentHolds: number | null;
@Column({ nullable: true })
instrumentNavTrack: number | null;
@Column({ nullable: true })
notes: string | null;
@OneToMany(() => TrackEntity, (track: TrackEntity) => track.log)
tracks: TrackEntity[]
@ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.logs, {onDelete: 'CASCADE', onUpdate: 'CASCADE'})
@JoinColumn({ name: 'pilotId' })
pilot: PilotEntity;
}

View File

@@ -0,0 +1,59 @@
import { CallHandler, ExecutionContext, NestInterceptor, UnauthorizedException } from '@nestjs/common';
import { Observable, map } from 'rxjs';
import { LogEntity } from './log.entity';
import { jwtDecode } from 'jwt-decode';
import { CustomJwtPayload } from 'src/interfaces/customJwtPayload.interface';
import { IS_PUBLIC_KEY } from '@noahspan/noahspan-modules';
import { Reflector } from '@nestjs/core';
export class LogInterceptor implements NestInterceptor {
constructor(private reflector: Reflector) {}
intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
return handler.handle().pipe(
map((data: LogEntity[]) => {
const req = context.switchToHttp().getRequest();
const limitData = (data) => {
return data.map((log: LogEntity) => {
return {
id: log.id,
pilot: {
name: log.pilot.name
},
date: log.date,
aircraftMakeModel: log.aircraftMakeModel,
routeFrom: log.routeFrom,
routeTo: log.routeTo,
durationOfFlight: log.durationOfFlight,
tracks: log.tracks,
};
});
}
if (req.headers.authorization) {
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1];
const jwtPayload: CustomJwtPayload = jwtDecode(token);
if (jwtPayload.roles.includes('Flying.Read')) {
const logs = limitData(data);
return logs;
} else {
return data;
}
} else if (!req.headers.authorization && isPublic) {
const publicData = limitData(data)
const logs = publicData.slice(0, 5)
return logs;
}
})
);
}
}

View File

@@ -1,28 +1,19 @@
import { Module } from '@nestjs/common';
import { LogController } from './log.controller';
import { LogService } from './log.service';
import { AzureTableStorageModule } from '@noahspan/azure-database';
import { Log } from './log.entity';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { LogEntity } from './log.entity';
import { ConfigService } from '@nestjs/config';
import { FileService } from '../file/file.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PilotModule } from 'src/pilot/pilot.module';
@Module({
imports: [
AzureTableStorageModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => {
return {
connectionString: configService.get<string>('azureStorageConnectionString')
};
},
inject: [ConfigService]
}),
AzureTableStorageModule.forFeature(Log, {
createTableIfNotExists: false,
table: 'logs'
}),
PilotModule,
TypeOrmModule.forFeature([LogEntity])
],
controllers: [LogController],
exports: [LogService],
providers: [
ConfigService,
FileService,

View File

@@ -1,34 +1,63 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository, Repository } from '@noahspan/azure-database';
import { Log } from './log.entity';
import { v4 as uuidv4 } from 'uuid';
import { InjectRepository } from '@nestjs/typeorm';
import { LogEntity } from './log.entity';
import { DeleteResult, InsertResult, Repository, UpdateResult } from 'typeorm';
import { LogDto } from './log.dto';
import { PilotService } from 'src/pilot/pilot.service';
import { PilotEntity } from 'src/pilot/pilot.entity';
import { CustomError } from 'src/error/customError';
import { FileService } from 'src/file/file.service';
@Injectable()
export class LogService {
constructor(
@InjectRepository(Log) private readonly logRepository: Repository<Log>
@InjectRepository(LogEntity) private readonly logRepository: Repository<LogEntity>,
private readonly fileService: FileService,
private readonly pilotService: PilotService,
) {}
async find(partitionKey: string, rowKey: string): Promise<Log> {
return await this.logRepository.find(partitionKey, rowKey);
async find(id: string): Promise<LogEntity> {
const logEntity: LogEntity = await this.logRepository.findOne({
where: { id: id },
relations: ['pilot', 'tracks']
});
return logEntity;
}
async findAll(): Promise<Log[]> {
return await this.logRepository.findAll();
async findAll(): Promise<LogEntity[]> {
return await this.logRepository.find({
relations: ['pilot', 'tracks']
});
}
async create(log: Log): Promise<Log> {
log.partitionKey = 'log';
log.rowKey = uuidv4();
async create(logDto: LogDto): Promise<InsertResult> {
try{
const pilotEntity: PilotEntity = await this.pilotService.find(logDto.pilotId);
return await this.logRepository.create(log);
if (pilotEntity) {
const { pilotId, ...newLogDto } = logDto;
const log = this.logRepository.create({
...newLogDto,
pilot: pilotEntity
})
return this.logRepository.insert(log);
} else {
throw new CustomError('Pilot not found', 'Not found', 404);
}
} catch (error) {
throw error
}
}
async update(partitionKey: string, rowKey: string, log: Log): Promise<Log> {
return await this.logRepository.update(partitionKey, rowKey, log);
async update(id: string, log: LogDto): Promise<UpdateResult> {
return await this.logRepository.update(id, log);
}
async delete(partitionKey: string, rowKey: string): Promise<void> {
await this.logRepository.delete(partitionKey, rowKey);
async delete(id: string): Promise<DeleteResult> {
await this.fileService.deleteFolder('tracks', id);
return await this.logRepository.delete({ id });
}
}

View File

@@ -3,15 +3,26 @@ import { AppModule } from './app.module';
import { HttpService } from '@nestjs/axios';
import { HttpExceptionFilter } from './filters/http-exception.filter';
import { InternalServerErrorException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import * as session from 'express-session';
async function bootstrap() {
const httpService = new HttpService();
const app = await NestFactory.create(AppModule);
app.enableCors();
app.enableCors({
origin: 'http://localhost:8080', // Allow requests from your frontend's origin
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
credentials: true, // If you need to send cookies or authorization headers
});
app.setGlobalPrefix('api');
app.useGlobalFilters(new HttpExceptionFilter());
app.use(
session({
secret: 'blah',
resave: false,
saveUninitialized: false
})
)
httpService.axiosRef.interceptors.response.use(
(response) => {

View File

@@ -0,0 +1,71 @@
import { Body, Controller, Delete, Get, HttpException, Param, Post, Put, UseGuards } from "@nestjs/common";
import { MedicalService } from "./medical.service";
import { CustomError } from "src/error/customError";
import { MedicalDto } from "./medical.dto";
import { AuthGuard } from "@noahspan/noahspan-modules";
@Controller('medical')
export class MedicalController {
constructor(private readonly medicalService: MedicalService) {}
@UseGuards(AuthGuard)
@Get(':id')
async find(@Param('id') id: string) {
try {
return await this.medicalService.find(id)
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
@UseGuards(AuthGuard)
@Get()
async findAll() {
try {
return await this.medicalService.findAll();
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
@UseGuards(AuthGuard)
@Post()
async create(@Body() medicalDto: MedicalDto) {
try {
return await this.medicalService.create(medicalDto);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
@UseGuards(AuthGuard)
@Put(':id')
async update(@Param('id') id: string, @Body() medicalDto: MedicalDto) {
try {
return await this.medicalService.update(id, medicalDto);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
@UseGuards(AuthGuard)
@Delete(':id')
async delete(@Param('id') id: string) {
try {
return await this.medicalService.delete(id);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
}

View File

@@ -0,0 +1,6 @@
export class MedicalDto {
id: string;
class: string;
expirationDate: string;
pilotId: string;
}

View File

@@ -0,0 +1,18 @@
import { PilotEntity } from 'src/pilot/pilot.entity';
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
@Entity({ name: 'medical' })
export class MedicalEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
class: string;
@Column()
expirationDate: Date;
@ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.medical, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
@JoinColumn({ name: 'pilotId' })
pilot: PilotEntity;
}

View File

View File

@@ -0,0 +1,33 @@
import { MedicalEntity } from "./medical.entity";
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { DeleteResult, InsertResult, Repository, UpdateResult } from 'typeorm';
import { MedicalDto } from "./medical.dto";
@Injectable()
export class MedicalService {
constructor(
@InjectRepository(MedicalEntity) private readonly medicalRepository: Repository<MedicalEntity>
) {}
async find(id: string): Promise<MedicalEntity> {
return await this.medicalRepository.findOneBy({ id });
}
async findAll(): Promise<MedicalEntity[]> {
return await this.medicalRepository.find();
}
async create(medical: MedicalDto): Promise<InsertResult> {
return await this.medicalRepository.insert(medical)
}
async update(id: string, medical: MedicalDto): Promise<UpdateResult> {
return await this.medicalRepository.update(id, medical);
}
async delete(id: string): Promise<DeleteResult> {
return await this.medicalRepository.delete({ id });
}
}

View File

@@ -1,5 +0,0 @@
export class Certificate {
type: string;
issueDate: string;
number: string;
}

View File

@@ -1,4 +0,0 @@
export class Endorsement {
type: string;
issueDate: Date;
}

View File

@@ -1,35 +0,0 @@
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
import { Observable, map } from 'rxjs';
export class PilotInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return handler.handle().pipe(
map((data) => {
if (data.length) {
const pilots = data.map((pilot) => {
return {
partitionKey: pilot.partitionKey,
rowKey: pilot.rowKey,
id: pilot.id,
name: pilot.name,
certificates: pilot.certificates,
endorsements: pilot.endorsements
};
});
return pilots;
} else {
return data;
}
})
);
}
return handler.handle().pipe(map((data) => data));
}
}

View File

@@ -11,24 +11,24 @@ import {
UseInterceptors,
} from '@nestjs/common';
import { PilotDto } from './pilot.dto';
import { Pilot } from './pilot.entity';
import { PilotService } from './pilot.service';
import { CustomError } from '../error/customError';
import { AuthGuard } from '@noahspan/noahspan-modules'
import { PilotInterceptor } from './interceptors/pilot.interceptor';
import { PilotInterceptor } from './pilot.interceptor';
import { AuthGuard, Public } from '@noahspan/noahspan-modules';
import { Reflector } from '@nestjs/core';
const reflector = new Reflector();
@Controller('pilots')
@UseInterceptors(new PilotInterceptor())
@UseInterceptors(new PilotInterceptor(reflector))
export class PilotController {
constructor(private readonly pilotService: PilotService) {}
@Get(':partitionKey/:rowKey')
async find(
@Param('partitionKey') partitionKey: string,
@Param('rowKey') rowKey: string
) {
@Get(':id')
@Public()
async find(@Param('id') id: string) {
try {
return await this.pilotService.find(partitionKey, rowKey);
return await this.pilotService.find(id);
} catch (error) {
const customError = error as CustomError;
@@ -37,75 +37,39 @@ export class PilotController {
}
@Get()
@Public()
async findAll() {
try {
return await this.pilotService.findAll();
} catch (error) {
const customError = error as CustomError;
console.log(error)
throw new HttpException(customError.message, customError.statusCode);
}
}
@UseGuards(AuthGuard)
@Post()
@UseGuards(AuthGuard)
async create(@Body() pilotDto: PilotDto) {
try {
let pilot = new Pilot();
pilot = {
partitionKey: pilotDto.partitionKey,
rowKey: pilotDto.rowKey,
id: pilotDto.id,
name: pilotDto.name,
address: pilotDto.address,
city: pilotDto.city,
state: pilotDto.state,
postalCode: pilotDto.postalCode,
email: pilotDto.email,
phone: pilotDto.phone,
medicalClass: pilotDto.medicalClass,
medicalExpiration: pilotDto.medicalExpiration,
certificates: JSON.stringify(pilotDto.certificates),
endorsements: JSON.stringify(pilotDto.endorsements)
}
return await this.pilotService.create(pilot);
return await this.pilotService.create(pilotDto);
} catch (error) {
const customError = error as CustomError;
console.log(error)
// const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
// throw new HttpException(customError.message, customError.statusCode);
}
}
@Put(':id')
@UseGuards(AuthGuard)
@Put(':partitionKey/:rowKey')
async update(
@Param('partitionKey') partitionKey: string,
@Param('rowKey') rowKey: string,
@Param('id') id: string,
@Body() pilotDto: PilotDto
) {
try {
let pilot = new Pilot();
pilot = {
partitionKey: pilotDto.partitionKey,
rowKey: pilotDto.rowKey,
id: pilotDto.id,
name: pilotDto.name,
address: pilotDto.address,
city: pilotDto.city,
state: pilotDto.state,
postalCode: pilotDto.postalCode,
email: pilotDto.email,
phone: pilotDto.phone,
medicalClass: pilotDto.medicalClass,
medicalExpiration: pilotDto.medicalExpiration,
certificates: JSON.stringify(pilotDto.certificates),
endorsements: JSON.stringify(pilotDto.endorsements)
}
return await this.pilotService.update(partitionKey, rowKey, pilot);
return await this.pilotService.update(id, pilotDto);
} catch (error) {
const customError = error as CustomError;
@@ -113,14 +77,13 @@ export class PilotController {
}
}
@Delete(':id')
@UseGuards(AuthGuard)
@Delete(':partitionKey/:rowKey')
async delete(
@Param('partitionKey') partitionKey: string,
@Param('rowKey') rowKey: string
@Param('id') id: string,
) {
try {
return await this.pilotService.delete(partitionKey, rowKey);
return await this.pilotService.delete(id);
} catch (error) {
const customError = error as CustomError;

View File

@@ -1,10 +1,6 @@
import { Certificate } from "./certificate/certificate.entity";
import { Endorsement } from "./endorsement/endorsement.entity";
import { LogEntity } from "src/log/log.entity";
export class PilotDto {
partitionKey: string;
rowKey: string;
id: string;
name: string;
address: string;
city: string;
@@ -12,8 +8,4 @@ export class PilotDto {
postalCode: string;
email?: string;
phone?: string;
medicalClass?: string;
medicalExpiration?: string;
certificates: Certificate;
endorsements: Endorsement
}

View File

@@ -1,18 +1,47 @@
import { EntityString } from '@noahspan/azure-database';
import { LogEntity } from 'src/log/log.entity';
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
import { CertificateEntity } from '../certificate/certificate.entity';
import { EndorsementEntity } from 'src/endorsement/endorsement.entity';
import { MedicalEntity } from 'src/medical/medical.entity';
export class Pilot {
@EntityString() partitionKey: string;
@EntityString() rowKey: string;
@EntityString() id: string;
@EntityString() name: string;
@EntityString() address?: string;
@EntityString() city?: string;
@EntityString() state?: string;
@EntityString() postalCode?: string;
@EntityString() email?: string;
@EntityString() phone?: string;
@EntityString() medicalClass?: string;
@EntityString() medicalExpiration: string;
@EntityString() certificates: string;
@EntityString() endorsements: string;
@Entity({ name: 'pilots' })
export class PilotEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
@Column()
address: string
@Column()
city: string;
@Column()
state: string;
@Column()
postalCode: string;
@Column()
email: string;
@Column()
phone: string;
@Column()
userId: string | null;
@OneToMany(() => LogEntity, (log: LogEntity) => log.pilot)
logs: LogEntity[];
@OneToMany(() => CertificateEntity, (certificate: CertificateEntity) => certificate.pilot)
certificates: CertificateEntity[];
@OneToMany(() => EndorsementEntity, (endorsement: EndorsementEntity) => endorsement.pilot)
endorsements: EndorsementEntity[];
@OneToMany(() => MedicalEntity, (medical: MedicalEntity) => medical.pilot)
medical: MedicalEntity[];
}

View File

@@ -0,0 +1,51 @@
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
import { jwtDecode } from 'jwt-decode';
import { Observable, map } from 'rxjs';
import { CustomJwtPayload } from 'src/interfaces/customJwtPayload.interface';
import { PilotEntity } from './pilot.entity';
import { IS_PUBLIC_KEY } from '@noahspan/noahspan-modules';
import { Reflector } from '@nestjs/core';
export class PilotInterceptor implements NestInterceptor {
constructor(private reflector: Reflector) {}
intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
return handler.handle().pipe(
map((data: PilotEntity[]) => {
const req = context.switchToHttp().getRequest();
const limitData = (data) => {
return data.map((pilot: PilotEntity) => {
return {
id: pilot.id,
name: pilot.name
};
})
}
console.log(req.headers.authorization)
if (req.headers.authorization) {
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1];
const jwtPayload: CustomJwtPayload = jwtDecode(token);
if (jwtPayload.roles.includes('Flying.Read')) {
const pilots = limitData(data)
return pilots;
} else {
return data;
}
} else if (!req.headers.authorization && isPublic) {
const publicData = limitData(data);
const logs = publicData.slice(0,5)
return logs;
}
})
);
}
}

View File

@@ -1,41 +1,15 @@
import { Module } from '@nestjs/common';
import { PilotController } from './pilot.controller';
import { PilotService } from './pilot.service';
import { AzureTableStorageModule } from '@noahspan/azure-database';
import { Pilot } from './pilot.entity';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { Log } from 'src/log/log.entity';
import { PilotEntity } from './pilot.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
AzureTableStorageModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => {
return {
connectionString: configService.get<string>('azureStorageConnectionString')
};
},
inject: [ConfigService]
}),
AzureTableStorageModule.forFeature(Log, {
createTableIfNotExists: false,
table: 'logs'
}),
AzureTableStorageModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => {
return {
connectionString: configService.get<string>('azureStorageConnectionString')
};
},
inject: [ConfigService]
}),
AzureTableStorageModule.forFeature(Pilot, {
createTableIfNotExists: false,
table: 'pilots'
})
TypeOrmModule.forFeature([PilotEntity])
],
controllers: [PilotController],
exports: [PilotService],
providers: [PilotService]
})
export class PilotModule {}

View File

@@ -1,54 +1,46 @@
import { InjectRepository, Repository } from '@noahspan/azure-database';
import { Inject, Injectable } from '@nestjs/common';
import { Pilot } from './pilot.entity';
import { Log } from 'src/log/log.entity';
import { LogService } from 'src/log/log.service';
import { Injectable } from '@nestjs/common';
import { PilotEntity } from './pilot.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { DeleteResult, InsertResult, Repository, UpdateResult } from 'typeorm';
import { PilotDto } from './pilot.dto';
import { CustomError } from 'src/error/customError';
@Injectable()
export class PilotService {
constructor(
@InjectRepository(Pilot) private readonly pilotRepository: Repository<Pilot>,
@InjectRepository(Log) private readonly logRepository: Repository<Log>
@InjectRepository(PilotEntity) private readonly pilotRepository: Repository<PilotEntity>
) {}
async find(partitionKey: string, rowKey: string): Promise<Pilot> {
return await this.pilotRepository.find(partitionKey, rowKey);
async find(id: string): Promise<PilotEntity> {
try {
const pilotEntity = await this.pilotRepository.findOneBy({ id });
if (pilotEntity) {
return pilotEntity
} else {
throw new CustomError('Pilot not found', 'Not found', 404)
}
} catch (error) {
throw error
}
}
async findAll(): Promise<Pilot[]> {
return await this.pilotRepository.findAll();
async findAll(): Promise<PilotEntity[]> {
return await this.pilotRepository.find();
}
async create(pilot: Pilot): Promise<Pilot> {
// try {
// return await this.pilotRepository.create(pilot);
// } catch (error) {
// throw new Error(error);
// }
return await this.pilotRepository.create(pilot);
async create(pilot: PilotDto): Promise<InsertResult> {
return await this.pilotRepository.insert(pilot);
}
async update(
partitionKey: string,
rowKey: string,
pilot: Pilot
): Promise<Pilot> {
return await this.pilotRepository.update(partitionKey, rowKey, pilot);
id: string,
pilot: PilotDto
): Promise<UpdateResult> {
return await this.pilotRepository.update(id, pilot);
}
async delete(partitionKey: string, rowKey: string): Promise<void> {
const pilotLogs: Log[] = await this.logRepository.findAll({
queryOptions: {
filter: `pilotId eq '${rowKey}'`
}
})
for (const pilotLog of pilotLogs) {
await this.logRepository.delete(pilotLog.partitionKey, pilotLog.rowKey);
}
await this.pilotRepository.delete(partitionKey, rowKey);
return
async delete(id: string): Promise<DeleteResult> {
return await this.pilotRepository.delete({ id });
}
}

View File

@@ -0,0 +1,80 @@
import {
Controller,
Delete,
Get,
HttpException,
Param,
Post,
Query,
UploadedFile,
UseGuards,
UseInterceptors
} from '@nestjs/common';
import { AuthGuard } from '@noahspan/noahspan-modules';
import { CustomError } from '../error/customError';
import { FileInterceptor } from '@nestjs/platform-express';
import { FileService } from '../file/file.service';
import { TrackService } from './track.service';
import { TrackEntity } from './track.entity';
import { DeleteResult, InsertResult } from 'typeorm';
@Controller('tracks')
export class TrackController {
constructor(
private readonly fileService: FileService,
private readonly trackService: TrackService
) {}
@UseGuards(AuthGuard)
@Get(':logId')
async findAll(@Param('logId') logId: string): Promise<TrackEntity[]> {
try {
return await this.trackService.findAll(logId);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
@UseGuards(AuthGuard)
@Post(':logId/:order')
@UseInterceptors(FileInterceptor('file'))
async create(@Param('logId') logId: string, @Param('order') order: number, @UploadedFile() file: Express.Multer.File): Promise<InsertResult> {
try {
return await this.trackService.create(logId, order, file);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
@UseGuards(AuthGuard)
@Delete(':id/:filename/:logId')
async delete(@Param('id') id: string, @Query('fileName') filename: string, @Query('logId') logId: string): Promise<DeleteResult> {
try {
return await this.trackService.delete(id, logId, filename);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
@Get(':logId/:fileName')
async downloadTrack(@Param('logId') logId: string, @Param('fileName') fileName: string): Promise<string> {
try {
return await this.trackService.downloadTrackFile(logId, fileName);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
}

View File

@@ -0,0 +1,5 @@
export class TrackDto {
logId: string;
order: number;
url?: string;
}

View File

@@ -0,0 +1,18 @@
import { LogEntity } from 'src/log/log.entity';
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
@Entity({ name: 'tracks' })
export class TrackEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
url: string;
@Column()
order: number;
@ManyToOne(() => LogEntity, (log: LogEntity) => log.tracks, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
@JoinColumn({ name: 'logId' })
log: LogEntity;
}

View File

@@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from "@nestjs/typeorm";
import { TrackEntity } from "./track.entity";
import { TrackController } from "./track.controller";
import { FileService } from "../file/file.service";
import { TrackService } from './track.service';
import { LogModule } from 'src/log/log.module';
@Module({
imports: [
LogModule,
TypeOrmModule.forFeature([TrackEntity])
],
controllers: [TrackController],
providers: [
FileService,
TrackService
]
})
export class TrackModule {}

View File

@@ -0,0 +1,98 @@
import { DeleteResult, InsertResult, Repository, UpdateResult } from "typeorm";
import { TrackDto } from "./track.dto";
import { TrackEntity } from "./track.entity";
import { InjectRepository } from "@nestjs/typeorm";
import { Injectable } from "@nestjs/common";
import { LogService } from "src/log/log.service";
import { LogEntity } from "src/log/log.entity";
import { CustomError } from "src/error/customError";
import { FileService } from "src/file/file.service";
@Injectable()
export class TrackService {
private readonly containerName: string = 'tracks'
constructor(
@InjectRepository(TrackEntity) private readonly trackRepository: Repository<TrackEntity>,
private readonly fileService: FileService,
private readonly logService: LogService
) {}
async find(id: string): Promise<TrackEntity> {
try {
return await this.trackRepository.findOneBy({ id });
} catch (error) {
throw new CustomError('Track not found', 'Not found', 404)
}
}
async findAll(logId: string): Promise<TrackEntity[]> {
try {
const logEntity: LogEntity = await this.logService.find(logId);
if (logEntity) {
const tracks = await this.trackRepository.find({
where: { log:
{
id: logEntity.id
}
},
})
return tracks;
}
} catch (error) {
console.log(error)
throw new CustomError('Tracks not found', 'Not found', 404);
}
}
async create(logId: string, order: number, file: Express.Multer.File): Promise<InsertResult> {
try {
const logEntity: LogEntity = await this.logService.find(logId);
if (logEntity) {
const url = await this.fileService.uploadFile(file, this.containerName, logId);
const track = this.trackRepository.create({
log: logEntity,
order: order,
url: url
});
return await this.trackRepository.insert(track);
} else {
throw new CustomError('Log not found', 'Not found', 404)
}
} catch (error) {
throw error;
}
}
async update(id: string, track: TrackDto): Promise<UpdateResult> {
try {
return await this.trackRepository.update(id, track);
} catch(error) {
throw error
}
}
async downloadTrackFile(logId: string, fileName: string): Promise<string> {
try {
const downloadedFile: string = await this.fileService.downloadFile(this.containerName, logId, fileName);
return downloadedFile;
} catch (error) {
throw error
}
}
async delete(id: string, logId: string, fileName: string): Promise<DeleteResult> {
try {
await this.fileService.deleteFile(this.containerName, logId, fileName);
return await this.trackRepository.delete({ id });
} catch (error) {
throw error
}
}
}