Compare commits

..

7 Commits

Author SHA1 Message Date
122c123ab8 Merge branch 'main' into feature/73-add-flights-page 2025-03-23 11:47:45 -05:00
b74e419646 adding flights page 2025-03-23 11:44:51 -05:00
3b2dbcf3aa removing unused dependencies 2025-03-23 11:26:34 -05:00
11fe779c4d adding flights page 2025-03-23 11:19:39 -05:00
f51c4f2e1a adding flights page 2025-03-23 11:06:35 -05:00
eb1bfb53c5 adding flights page 2025-03-23 10:28:04 -05:00
88b7f77809 adding flights page 2025-03-23 10:23:30 -05:00
217 changed files with 16923 additions and 24744 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -10,16 +10,21 @@ on:
type: string
jobs:
build-and-test:
build:
runs-on: ubuntu-latest
environment: ${{ inputs.environment_name }}
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
sparse-checkout: |
api
client
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 10
run_install: false
- name: Install Nest CLI
run: |
@@ -32,26 +37,16 @@ jobs:
- name: Install dependencies
run: |
npm ci
pnpm install
- name: Build API
- name: Build
run: |
npm run build -w api
pnpm --filter api build
- name: Test API
- name: Deploy
if: ${{ github.event_name != 'pull_request' }}
run: |
npm run test -w api
- name: Build Client
env:
VITE_API_URL: ${{ vars.VITE_API_URL }}
VITE_BASE_URL: ${{ vars.VITE_BASE_URL }}
VITE_CLIENT_ID: ${{ vars.VITE_CLIENT_ID }}
VITE_ISSUER_URI: ${{ vars.VITE_ISSUER_URI }}
VITE_TENANT_ID: ${{ vars.VITE_TENANT_ID }}
run: |
printenv
npm run build -w client
pnpm --filter api --prod deploy ./.prod/api
- name: Log into Docker Hub
if: ${{ github.event_name != 'pull_request' }}
@@ -69,5 +64,6 @@ jobs:
uses: docker/build-push-action@v6
with:
push: true
tags: noahspan/flying:${{ inputs.version_number }}
tags: noahspan/flying-api:${{ inputs.version_number }}
context: .
target: api

70
.github/workflows/app_build.yaml vendored Normal file
View File

@@ -0,0 +1,70 @@
name: 'App Build'
on:
workflow_call:
inputs:
environment_name:
required: true
type: string
version_number:
required: true
type: string
env:
VITE_API_URL: ${{ vars.VITE_API_URL }}
VITE_CLIENT_ID: ${{ vars.VITE_CLIENT_ID }}
VITE_TENANT_ID: ${{ vars.VITE_TENANT_ID }}
VITE_REDIRECT_URL: ${{ vars.VITE_REDIRECT_URL }}
jobs:
build:
runs-on: ubuntu-latest
environment: ${{ inputs.environment_name }}
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: |
app
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 10
run_install: false
- name: 'Setup Node'
uses: actions/setup-node@v4
with:
node-version: ${{ vars.NODE_VERSION }}
- name: Install
run: |
pnpm install
- name: Build
run: |
pnpm --filter app build
- name: Deploy
if: ${{ github.event_name != 'pull_request' }}
run: |
pnpm --filter app --prod deploy ./.prod/app
- name: Setup Docker Buildx
if: ${{ github.event_name != 'pull_request' }}
uses: docker/setup-buildx-action@v3
- name: Log into Docker Hub
if: ${{ github.event_name != 'pull_request' }}
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push to registry
if: ${{ github.event_name != 'pull_request' }}
uses: docker/build-push-action@v6
with:
push: true
tags: noahspan/flying-app:${{ inputs.version_number }}
context: .
target: app

View File

@@ -4,8 +4,8 @@ on:
outputs:
api:
value: ${{ jobs.changes.outputs.api }}
client:
value: ${{ jobs.changes.outputs.client }}
app:
value: ${{ jobs.changes.outputs.app }}
jobs:
changes:
@@ -13,15 +13,16 @@ jobs:
runs-on: ubuntu-latest
outputs:
api: ${{ steps.filter.outputs.api }}
client: ${{ steps.filter.outputs.client }}
app: ${{ steps.filter.outputs.app }}
steps:
- uses: actions/checkout@v3
- uses: dorny/paths-filter@v3
id: filter
with:
initial-fetch-depth: 2
filters: |
api:
- 'api/**'
client:
- 'client/**'
app:
- 'app/**'

View File

@@ -23,13 +23,13 @@ jobs:
- name: Log in to Azure
uses: azure/login@v2
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
client-id: ${{ vars.VITE_CLIENT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
tenant-id: ${{ vars.VITE_TENANT_ID }}
- name: Azure CLI script
uses: azure/cli@v2
with:
azcliversion: latest
inlineScript: |
az containerapp update --name ${{ inputs.app_name }}-${{ inputs.environment_name }} --container-name ${{ inputs.app_name }} --resource-group noahspan --image docker.io/noahspan/${{ inputs.app_name }}:${{ inputs.version_number }}
az containerapp update --name ${{ inputs.app_name }}-${{ inputs.environment_name }} --resource-group noahspan-flying --image docker.io/noahspan/${{ inputs.app_name }}:${{ inputs.version_number }}

View File

@@ -8,25 +8,48 @@ jobs:
changes:
uses: ./.github/workflows/changes.yaml
build:
if: ${{ needs.changes.outputs.api == 'true' || needs.changes.outputs.client == 'true' }}
name: build
build-api:
if: ${{ needs.changes.outputs.api == 'true' }}
name: build-api
needs:
- changes
uses: ./.github/workflows/build_and_test.yaml
uses: ./.github/workflows/api_build.yaml
with:
environment_name: test
version_number: ${{ github.run_id }}
secrets: inherit
deploy:
name: deploy
deploy-api:
name: deploy-api
needs:
- changes
- build
- build-api
uses: ./.github/workflows/deploy.yaml
with:
app_name: flying
app_name: flying-api
environment_name: test
version_number: ${{ github.run_id }}
secrets: inherit
build-app:
if: ${{ needs.changes.outputs.app == 'true' }}
name: build-app
needs:
- changes
uses: ./.github/workflows/app_build.yaml
with:
environment_name: test
version_number: ${{ github.run_id }}
secrets: inherit
deploy-app:
name: deploy-app
needs:
- changes
- build-app
uses: ./.github/workflows/deploy.yaml
with:
app_name: flying-app
environment_name: test
version_number: ${{ github.run_id }}
secrets: inherit

View File

@@ -7,12 +7,23 @@ jobs:
changes:
uses: ./.github/workflows/changes.yaml
build:
if: ${{ needs.changes.outputs.app == 'true' || needs.changes.outputs.client == 'true'}}
name: build
build-api:
if: ${{ needs.changes.outputs.api == 'true' }}
name: build-api
needs:
- changes
uses: ./.github/workflows/build_and_test.yaml
uses: ./.github/workflows/api_build.yaml
with:
environment_name: pull_request
version_number: ${{ github.run_id }}
secrets: inherit
build-app:
if: ${{ needs.changes.outputs.app == 'true' }}
name: build-app
needs:
- changes
uses: ./.github/workflows/app_build.yaml
with:
environment_name: pull_request
version_number: ${{ github.run_id }}

View File

@@ -2,24 +2,43 @@ name: Tag
on:
push:
tags:
- '**'
- 'v[0-9]+.[0-9]+.[0-9]+'
jobs:
build:
name: build
uses: ./.github/workflows/build_and_test.yaml
build-api:
name: build-api
uses: ./.github/workflows/api_build.yaml
with:
environment_name: prod
version_number: ${{ github.ref_name }}
secrets: inherit
deploy:
name: deploy
deploy-api:
name: deploy-api
needs:
- build
- build-api
uses: ./.github/workflows/deploy.yaml
with:
app_name: flying
app_name: flying-api
environment_name: prod
version_number: ${{ github.ref_name }}
secrets: inherit
build-app:
name: build-app
uses: ./.github/workflows/app_build.yaml
with:
environment_name: prod
version_number: ${{ github.ref_name }}
secrets: inherit
deploy-app:
name: deploy-app
needs:
- build-app
uses: ./.github/workflows/deploy.yaml
with:
app_name: flying-app
environment_name: prod
version_number: ${{ github.ref_name }}
secrets: inherit

View File

@@ -1,16 +1,39 @@
FROM node:22
# FROM --platform=linux/amd64 node:18-alpine AS base
# ENV PNPM_HOME="/pnpm"
# ENV PATH="$PNPM_HOME:$PATH"
# RUN corepack enable
WORKDIR app
COPY ./api/dist ./api/dist
COPY ./api/package.json package-lock.json ./api
COPY ./client/dist ./client/dist
# FROM base AS api
# COPY /api/dist /app/dist
# COPY /api/node_modules /app/node_modules
# WORKDIR /app
# EXPOSE 3000
# # CMD ["node", "dist/main.js"]
# ENTRYPOINT ["tail", "-f", "/dev/null"]
# FROM base AS app
# COPY /app/dist /app/dist
# WORKDIR /app
# RUN npm i -g serve
# EXPOSE 8080
# CMD [ "serve", "-s", "/dist", "-p", "8080" ]
FROM node:18-slim AS base
FROM base AS api
WORKDIR api
RUN npm ci
WORKDIR /
COPY ./.prod/api .
EXPOSE 3000
CMD ["node", "dist/main.js"]
# ENTRYPOINT ["tail", "-f", "/dev/null"]
CMD ["node", "./app/api/dist/main.js"]
# ENTRYPOINT ["tail", "-f", "/dev/null"]
FROM base AS app
WORKDIR /app
COPY ./.prod/app .
RUN npm i -g serve
EXPOSE 8080
CMD [ "serve", "-s", "dist", "-p", "8080" ]

3
api/.gitignore vendored
View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "api",
"version": "2.0.0",
"version": "1.3.0",
"description": "",
"author": "",
"private": true,
@@ -16,40 +16,24 @@
"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",
"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"
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@azure/storage-blob": "^12.27.0",
"@microsoft/microsoft-graph-client": "^3.0.7",
"@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",
"@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",
"@noahspan/azure-database": "^3.1.2",
"@noahspan/noahspan-modules": "^1.2.11",
"@noahspan/noahspan-modules": "^1.1.5",
"@schematics/angular": "^17.3.7",
"@types/multer": "^1.4.12",
"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",
"dotenv": "^16.4.7",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"typeorm": "^0.3.25",
"uuid": "^10.0.0",
"uuidv4": "^6.2.13"
},
@@ -57,13 +41,11 @@
"@microsoft/microsoft-graph-types": "^2.40.0",
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^11.1.6",
"@nestjs/testing": "^10.0.0",
"@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

@@ -0,0 +1,22 @@
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,17 +1,12 @@
import { Module } from '@nestjs/common';
import { HealthModule } from './health/health.module';
import { FeatureFlagModule } from './featureFlag/feature-flag.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 { AuthModule, MsGraphModule } from '@noahspan/noahspan-modules';
import { AuthGuard, AuthModule, UserModule } 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: [
@@ -20,42 +15,35 @@ import { join } from 'path';
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => {
return {
audience: configService.get<string>('audience'),
issuerUrl: configService.get<string>('issuer'),
jwksUri: configService.get<string>('jwksUri')
clientId: configService.get<string>('clientId'),
clientSecret: configService.get<string>('clientSecret'),
tenantId: configService.get<string>('tenantId')
};
},
}),
ConfigModule.forRoot({
isGlobal: true,
load: [configuration]
}),
HealthModule,
FeatureFlagModule,
LogModule,
MsGraphModule.registerAsync({
PilotModule,
UserModule.registerAsync({
inject: [ConfigService],
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => {
return {
authority: configService.get<string>('authority'),
clientId: configService.get<string>('clientId'),
clientSecret: configService.get<string>('clientSecret'),
tenantId: configService.get<string>('tenantId')
}
}
}),
PilotModule,
ServeStaticModule.forRoot({
rootPath: join(__dirname, '../..', 'client', 'dist')
}),
TrackModule,
TypeOrmModule.forRoot(dataSourceOptions)
})
],
providers: [
{
provide: APP_FILTER,
useClass: HttpExceptionFilter
}
},
]
})
export class AppModule {}

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
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

@@ -0,0 +1,25 @@
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

@@ -1,21 +0,0 @@
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,10 +1,6 @@
export default () => ({
azureStorageConnectionString: process.env.AZURE_STORAGE_CONNECTION_STRING,
audience: process.env.AUDIENCE,
authority: process.env.AUTHORITY,
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

@@ -1,20 +0,0 @@
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'),
migrationsRun: true
}
const dataSource = new DataSource(dataSourceOptions);
export default dataSource;

View File

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

View File

@@ -1,64 +0,0 @@
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

@@ -1,65 +0,0 @@
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

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

View File

@@ -1,18 +0,0 @@
import { PilotEntity } from '../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

@@ -1,32 +0,0 @@
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

@@ -0,0 +1,41 @@
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

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

View File

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

View File

@@ -0,0 +1,27 @@
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

@@ -0,0 +1,18 @@
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, logId: string): Promise<string> {
async uploadFile(file: Express.Multer.File, containerName: string, rowKey: string): Promise<string> {
this.containerName = containerName;
const blockBlobClient = await this.getBlobClient(`${logId}/${file.originalname}`);
const blockBlobClient = await this.getBlobClient(`${rowKey}/${file.originalname}`);
const fileUrl = blockBlobClient.url;
await blockBlobClient.uploadData(file.buffer);
@@ -48,42 +48,21 @@ import { ConfigService } from '@nestjs/config';
return fileUrl;
}
async downloadFile(containerName: string, logId: string, fileName: string): Promise<string> {
async downloadFile(containerName: string, rowKey: string, fileName: string): Promise<string> {
this.containerName = containerName;
const blockBlobClient = await this.getBlobClient(`${logId}/${fileName}`);
const blockBlobClient = await this.getBlobClient(`${rowKey}/${fileName}`);
const downloadBlockBlobResponse = await blockBlobClient.download();
const downloaded: string = (await this.streamToBuffer(downloadBlockBlobResponse.readableStreamBody)).toString()
return downloaded
}
async deleteFile(containerName: string, logId: string, fileName: string): Promise<void> {
async deleteFile(containerName: string, rowKey:string, fileName: string): Promise<void> {
this.containerName = containerName;
const blockBlobClient = await this.getBlobClient(`${logId}/${fileName}`);
const blockBlobClient = await this.getBlobClient(`${rowKey}/${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

@@ -1,25 +0,0 @@
import { Test, TestingModule } from "@nestjs/testing";
import { HealthController } from "./health.controller"
import { HttpStatus } from "@nestjs/common";
describe('HealthController', () => {
let controller: HealthController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [HealthController]
}).compile();
controller = module.get<HealthController>(HealthController);
})
it('should be defined', () => {
expect(controller).toBeDefined();
})
it('isHealth => should return status ok', async () => {
const result = await controller.isHealthy();
expect(result).toEqual(HttpStatus.OK);
})
})

View File

@@ -1,13 +0,0 @@
import {
Controller,
Get,
HttpStatus,
} from '@nestjs/common';
@Controller('health')
export class HealthController {
@Get()
async isHealthy(): Promise<HttpStatus> {
return HttpStatus.OK
}
}

View File

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

View File

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

View File

@@ -0,0 +1,41 @@
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

@@ -1,200 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { LogController } from './log.controller';
import { LogService } from './log.service';
import { LogDto } from './log.dto';
import { LogEntity } from './log.entity';
import { FileService } from '../file/file.service';
import { ConfigService } from '@nestjs/config';
describe('LogController', () => {
let controller: LogController;
const mockLogService = {
create: jest.fn(),
delete: jest.fn(),
find: jest.fn(),
findAll: jest.fn(),
update: jest.fn()
}
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [LogController],
providers: [
ConfigService,
FileService,
{
provide: LogService,
useValue: mockLogService
}
]
}).compile()
controller = module.get<LogController>(LogController);
})
it('should be defined', () => {
expect(controller).toBeDefined();
})
it('find => should find a log by id', async () => {
const id: string = '95834f84-0a02-44d3-884e-a20237adeca0';
const log = {
} as LogEntity
jest.spyOn(mockLogService, 'find').mockReturnValue(log);
const result = await controller.find(id);
expect(result).toEqual(log);
expect(mockLogService.find).toHaveBeenCalled();
expect(mockLogService.find).toHaveBeenCalledWith(id);
})
it('find => should fail to find a log by id', async () => {
const id: string = '95834f84-0a02-44d3-884e-a20237adeca0';
jest.spyOn(mockLogService, 'find').mockRejectedValue(new Error('Log not found'))
try {
await controller.find(id);
fail('find did not throw error')
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect(mockLogService.find).toHaveBeenCalled();
expect(mockLogService.find).toHaveBeenCalledWith(id);
expect(mockLogService.find).rejects.toThrow('Log not found')
}
})
it('findAll => should find all logs', async () => {
const id: string = '95834f84-0a02-44d3-884e-a20237adeca0';
const log = {
} as LogEntity;
const logs = [log]
jest.spyOn(mockLogService, 'findAll').mockReturnValue(logs);
const result = await controller.findAll();
expect(result).toEqual(logs);
expect(mockLogService.findAll).toHaveBeenCalled();
})
it('findAll => should fail to find all logs', async () => {
jest.spyOn(mockLogService, 'findAll').mockRejectedValue(new Error('Logs not found'))
try {
await controller.findAll();
fail('findAll did not throw error');
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect(mockLogService.findAll).toHaveBeenCalled();
expect(mockLogService.findAll).rejects.toThrow('Logs not found')
}
})
it('create => should create a new log', async () => {
const logDto = {
} as LogDto;
const log = {
} as LogEntity;
jest.spyOn(mockLogService, 'create').mockReturnValue(log);
const result = await controller.create(logDto);
expect(mockLogService.create).toHaveBeenCalled();
expect(mockLogService.create).toHaveBeenCalledWith(logDto);
expect(result).toEqual(log);
})
it('create => should failt to create a new log', async () => {
const logDto = {
} as LogDto;
jest.spyOn(mockLogService, 'create').mockRejectedValue(new Error('Log failed to create'))
try {
await controller.create(logDto);
fail('create did not throw error')
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect(mockLogService.create).toHaveBeenCalled();
expect(mockLogService.create).toHaveBeenCalledWith(logDto);
expect(mockLogService.create).rejects.toThrow('Log failed to create')
}
})
it('update => should update an existing log', async () => {
const id: string = '95834f84-0a02-44d3-884e-a20237adeca0';
const logDto = {
} as LogDto
jest.spyOn(mockLogService, 'update').mockReturnValue(logDto);
const result = await controller.update(id, logDto);
expect(result).toEqual(logDto);
expect(mockLogService.update).toHaveBeenCalled();
expect(mockLogService.update).toHaveBeenCalledWith(id, logDto);
})
it('update => should fail to update an exising log', async () => {
const id: string = '95834f84-0a02-44d3-884e-a20237adeca0';
const logDto = {
} as LogDto;
jest.spyOn(mockLogService, 'update').mockRejectedValue(new Error('Log failed to update'))
try {
await controller.update(id, logDto);
fail('update did not throw error')
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect(mockLogService.update).toHaveBeenCalled();
expect(mockLogService.update).toHaveBeenCalledWith(id, logDto);
expect(mockLogService.update).rejects.toThrow('Log failed to update')
}
})
it('delete => should delete and existing log', async () => {
const id: string = '95834f84-0a02-44d3-884e-a20237adeca0';
jest.spyOn(mockLogService, 'delete');
const result = await controller.delete(id);
expect(mockLogService.delete).toHaveBeenCalled();
expect(mockLogService.delete).toHaveBeenCalledWith(id);
})
it('delete => should fail to delete and exising log', async () => {
const id: string = '95834f84-0a02-44d3-884e-a20237adeca0';
jest.spyOn(mockLogService, 'delete').mockRejectedValue(new Error('Log failed to delete'));
try {
await controller.delete(id);
fail('delete did not throw error')
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect(mockLogService.delete).toHaveBeenCalled();
expect(mockLogService.delete).toHaveBeenCalledWith(id);
expect(mockLogService.delete).rejects.toThrow('Log failed to delete')
}
})
})

View File

@@ -7,23 +7,22 @@ import {
Param,
Post,
Put,
Query,
StreamableFile,
UploadedFile,
UseGuards,
UseInterceptors
} from '@nestjs/common';
import { LogDto } from './log.dto';
import { LogEntity } from './log.entity';
import { Log } from './log.entity';
import { LogService } from './log.service';
import { CustomError } from '../error/customError';
import { AuthGuard, Public } from '@noahspan/noahspan-modules';
import { LogInterceptor } from './log.interceptor';
import { AuthGuard } from '@noahspan/noahspan-modules';
import { LogInterceptor } from './interceptors/log.interceptor';
import { FileService } from '../file/file.service';
import { DeleteResult, InsertResult, UpdateResult } from 'typeorm';
import { Reflector } from '@nestjs/core';
const reflector = new Reflector();
import { FileInterceptor } from '@nestjs/platform-express';
@Controller('logs')
@UseInterceptors(new LogInterceptor(reflector))
export class LogController {
constructor(
private readonly fileService: FileService,
@@ -31,13 +30,14 @@ export class LogController {
) {}
@Get(':id')
@Public()
@Get(':partitionKey/:rowKey')
@UseInterceptors(new LogInterceptor())
async find(
@Param('id') id: string,
): Promise<LogEntity> {
@Param('partitionKey') partitionKey: string,
@Param('rowKey') rowKey: string
): Promise<Log> {
try {
return await this.logService.find(id);
return await this.logService.find(partitionKey, rowKey);
} catch (error) {
const customError = error as CustomError;
@@ -46,8 +46,8 @@ export class LogController {
}
@Get()
@Public()
async findAll(): Promise<LogEntity[]> {
@UseInterceptors(new LogInterceptor())
async findAll(): Promise<Log[]> {
try {
return await this.logService.findAll();
} catch (error) {
@@ -57,12 +57,15 @@ export class LogController {
}
}
@UseGuards(AuthGuard)
@Post()
@UseGuards(AuthGuard)
async create(@Body() logDto: LogDto) {
async create(@Body() logDto: LogDto): Promise<Log> {
try {
return await this.logService.create(logDto);
const log = new Log();
Object.assign(log, logDto);
return await this.logService.create(log);
} catch (error) {
const customError = error as CustomError;
@@ -70,14 +73,19 @@ export class LogController {
}
}
@Put(':id')
@UseGuards(AuthGuard)
@UseGuards(AuthGuard)
@Put(':partitionKey/:rowKey')
async update(
@Param('id') id: string,
@Param('partitionKey') partitionKey: string,
@Param('rowKey') rowKey: string,
@Body() logDto: LogDto
) {
): Promise<Log> {
try {
return await this.logService.update(id, logDto);
const log = new Log();
Object.assign(log, logDto);
return await this.logService.update(partitionKey, rowKey, log);
} catch (error) {
const customError = error as CustomError;
@@ -85,13 +93,52 @@ export class LogController {
}
}
@Delete(':id')
@UseGuards(AuthGuard)
@Delete(':partitionKey/:rowKey')
async delete(
@Param('id') id: string,
): Promise<DeleteResult> {
@Param('partitionKey') partitionKey: string,
@Param('rowKey') rowKey: string
): Promise<void> {
try {
return await this.logService.delete(id);
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 }
} catch (error) {
const customError = error as CustomError;
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;
}
@UseGuards(AuthGuard)
@Delete(':partitionKey/:rowKey/track')
async deleteTrack(@Param('rowKey') rowKey: string, @Query('fileName') fileName: string): Promise<void> {
try {
const containerName = 'tracks';
return await this.fileService.deleteFile(containerName, rowKey, fileName)
} catch (error) {
const customError = error as CustomError;

View File

@@ -1,26 +1,27 @@
export class LogDto {
pilotId: string;
date: Date;
pilotName: string;
date: string;
aircraftMakeModel: string;
aircraftIdentity: string;
routeFrom: string;
routeTo: 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?: []
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;
}

View File

@@ -1,165 +1,29 @@
import { PilotEntity } from '../pilot/pilot.entity';
import { TrackEntity } from '../track/track.entity';
import { ColumnNumericTransformer } from '../transformers/columnNumeric.transformer';
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, OneToMany } from 'typeorm';
@Entity({ name: 'logs' })
export class LogEntity {
@PrimaryGeneratedColumn('uuid')
id: string
@Column()
export class Log {
partitionKey: string;
rowKey: string;
pilotId: string;
@Column()
date: Date;
@Column()
pilotName: string;
date: string;
aircraftMakeModel: string;
@Column()
aircraftIdentity: string;
@Column()
routeFrom: string;
@Column()
routeTo: string;
@Column('numeric', {
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer()
})
durationOfFlight: number;
@Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
durationOfFlight: number | null;
singleEngineLand: number | null;
@Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
simulatorAtd: number | null;
@Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
landingsDay: number | null;
@Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
landingsNight: number | null;
@Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
groundTrainingReceived: number | null;
@Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
flightTrainingReceived: number | null;
@Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
crossCountry: number | null;
@Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
night: number | null;
@Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
solo: number | null;
@Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
pilotInCommand: number | null;
@Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
instrumentActual: number | null;
@Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
instrumentSimulated: number | null;
@Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
instrumentApproaches: number | null;
@Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
instrumentHolds: number | null;
@Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
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;
}
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;
}

View File

@@ -1,59 +0,0 @@
import { CallHandler, ExecutionContext, NestInterceptor, UnauthorizedException } from '@nestjs/common';
import { Observable, map } from 'rxjs';
import { LogEntity } from './log.entity';
import { jwtDecode } from 'jwt-decode';
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 = jwtDecode(token);
const rolesKeyName = Object.keys(jwtPayload).find((key) => key.includes('roles'));
if (jwtPayload[rolesKeyName].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,19 +1,28 @@
import { Module } from '@nestjs/common';
import { LogController } from './log.controller';
import { LogService } from './log.service';
import { LogEntity } from './log.entity';
import { ConfigService } from '@nestjs/config';
import { AzureTableStorageModule } from '@noahspan/azure-database';
import { Log } from './log.entity';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { FileService } from '../file/file.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PilotModule } from '../pilot/pilot.module';
@Module({
imports: [
PilotModule,
TypeOrmModule.forFeature([LogEntity])
AzureTableStorageModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => {
return {
connectionString: configService.get<string>('azureStorageConnectionString')
};
},
inject: [ConfigService]
}),
AzureTableStorageModule.forFeature(Log, {
createTableIfNotExists: false,
table: 'logs'
}),
],
controllers: [LogController],
exports: [LogService],
providers: [
ConfigService,
FileService,

View File

@@ -1,288 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { LogService } from './log.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { LogEntity } from './log.entity';
import { LogDto } from './log.dto';
import { FileService } from '../file/file.service';
import { ConfigService } from '@nestjs/config';
import { PilotService } from '../pilot/pilot.service';
import { PilotEntity } from '../pilot/pilot.entity';
describe('LogService', () => {
let service: LogService;
const mockFileService = {
deleteFolder: jest.fn()
}
const mockLogRepository = {
delete: jest.fn(),
find: jest.fn(),
findOne: jest.fn(),
findOneBy: jest.fn(),
save: jest.fn(),
}
const mockPilotRepository = {
create: jest.fn(),
delete: jest.fn(),
find: jest.fn(),
findAll: jest.fn(),
update: jest.fn()
}
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ConfigService,
LogService,
PilotService,
{
provide: FileService,
useValue: mockFileService
},
{
provide: getRepositoryToken(LogEntity),
useValue: mockLogRepository
},
{
provide: getRepositoryToken(PilotEntity),
useValue: mockPilotRepository
}
]
}).compile();
service = module.get<LogService>(LogService);
})
it('should be defined', () => {
expect(service).toBeDefined();
})
it('create => shoule create a log entry', async () => {
const logDto = {
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
date: new Date('2026-02-21'),
aircraftMakeModel: 'Cessna 172M',
aircraftIdentity: 'N12345',
routeFrom: 'KMSP',
routeTo: 'KMSP',
durationOfFlight: 1,
singleEngineLand: 1,
simulatorAtd: null,
landingsDay: 1,
landingsNight: null,
groundTrainingReceived: null,
flightTrainingReceived: null,
crossCountry: 1,
night: null,
solo: 1,
pilotInCommand: 1,
instrumentActual: null,
instrumentSimulated: null,
instrumentApproaches: null,
instrumentHolds: null,
instrumentNavTrack: null,
notes: 'This is a test'
} as LogDto
jest.spyOn(mockLogRepository, 'save').mockReturnValue(logDto);
const result = await service.create(logDto);
expect(mockLogRepository.save).toHaveBeenCalled();
expect(mockLogRepository.save).toHaveBeenCalledWith(logDto);
})
it('delete => should delete a log entry', async () => {
const id: string = '';
const log = {
id: 'd685f1ca-28e0-40b9-8713-74467db12965',
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
date: new Date('2026-02-21'),
aircraftMakeModel: 'Cessna 172M',
aircraftIdentity: 'N12345',
routeFrom: 'KMSP',
routeTo: 'KMSP',
durationOfFlight: 1,
singleEngineLand: 1,
simulatorAtd: null,
landingsDay: 1,
landingsNight: null,
groundTrainingReceived: null,
flightTrainingReceived: null,
crossCountry: 1,
night: null,
solo: 1,
pilotInCommand: 1,
instrumentActual: null,
instrumentSimulated: null,
instrumentApproaches: null,
instrumentHolds: null,
instrumentNavTrack: null,
notes: 'This is a test',
tracks: []
} as LogEntity;
jest.spyOn(mockFileService, 'deleteFolder').mockReturnValue(undefined);
jest.spyOn(mockLogRepository, 'delete').mockReturnValue(log);
const result = await service.delete(id);
expect(result).toEqual(log);
expect(mockLogRepository.delete).toHaveBeenCalled();
expect(mockLogRepository.delete).toHaveBeenCalledWith({ id: id })
})
it('find => should find a log entry by id', async () => {
const id: string = 'd685f1ca-28e0-40b9-8713-74467db12965';
const log = {
id: 'd685f1ca-28e0-40b9-8713-74467db12965',
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
date: new Date('2026-02-21'),
aircraftMakeModel: 'Cessna 172M',
aircraftIdentity: 'N12345',
routeFrom: 'KMSP',
routeTo: 'KMSP',
durationOfFlight: 1,
singleEngineLand: 1,
simulatorAtd: null,
landingsDay: 1,
landingsNight: null,
groundTrainingReceived: null,
flightTrainingReceived: null,
crossCountry: 1,
night: null,
solo: 1,
pilotInCommand: 1,
instrumentActual: null,
instrumentSimulated: null,
instrumentApproaches: null,
instrumentHolds: null,
instrumentNavTrack: null,
notes: 'This is a test',
tracks: []
} as LogEntity;
jest.spyOn(mockLogRepository, 'findOne').mockReturnValue(log);
const result = await service.find(id);
expect(result).toEqual(log);
expect(mockLogRepository.findOne).toHaveBeenCalled();
expect(mockLogRepository.findOne).toHaveBeenCalledWith({
relations: [
'pilot',
'tracks'
],
where: {
id: id
}
})
})
it('findAll => should find all log entries', async () => {
const log = {
id: 'd685f1ca-28e0-40b9-8713-74467db12965',
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
date: new Date('2026-02-21'),
aircraftMakeModel: 'Cessna 172M',
aircraftIdentity: 'N12345',
routeFrom: 'KMSP',
routeTo: 'KMSP',
durationOfFlight: 1,
singleEngineLand: 1,
simulatorAtd: null,
landingsDay: 1,
landingsNight: null,
groundTrainingReceived: null,
flightTrainingReceived: null,
crossCountry: 1,
night: null,
solo: 1,
pilotInCommand: 1,
instrumentActual: null,
instrumentSimulated: null,
instrumentApproaches: null,
instrumentHolds: null,
instrumentNavTrack: null,
notes: 'This is a test',
tracks: []
} as LogEntity;
const logs = [log]
jest.spyOn(mockLogRepository, 'find').mockReturnValue(logs);
const result = await service.findAll();
expect(result).toEqual(logs);
expect(mockLogRepository.find).toHaveBeenCalled();
})
it('update => should update a log entry', async () => {
const id: string = 'd685f1ca-28e0-40b9-8713-74467db12965';
const logDto = {
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
date: new Date('2026-02-21'),
aircraftMakeModel: 'Cessna 172M',
aircraftIdentity: 'N12345',
routeFrom: 'KMSP',
routeTo: 'KMSP',
durationOfFlight: 1,
singleEngineLand: 1,
simulatorAtd: null,
landingsDay: 1,
landingsNight: null,
groundTrainingReceived: null,
flightTrainingReceived: null,
crossCountry: 1,
night: null,
solo: 1,
pilotInCommand: 1,
instrumentActual: null,
instrumentSimulated: null,
instrumentApproaches: null,
instrumentHolds: null,
instrumentNavTrack: null,
notes: 'This is a test'
} as LogDto
const log = {
id: 'd685f1ca-28e0-40b9-8713-74467db12965',
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
date: new Date('2026-02-21'),
aircraftMakeModel: 'Cessna 172M',
aircraftIdentity: 'N12345',
routeFrom: 'KMSP',
routeTo: 'KMSP',
durationOfFlight: 1,
singleEngineLand: 1,
simulatorAtd: null,
landingsDay: 1,
landingsNight: null,
groundTrainingReceived: null,
flightTrainingReceived: null,
crossCountry: 1,
night: null,
solo: 1,
pilotInCommand: 1,
instrumentActual: null,
instrumentSimulated: null,
instrumentApproaches: null,
instrumentHolds: null,
instrumentNavTrack: null,
notes: 'This is a test',
tracks: []
} as LogEntity;
jest.spyOn(mockLogRepository, 'findOneBy').mockReturnValue(log);
jest.spyOn(mockLogRepository, 'save').mockReturnValue(logDto);
const result = await service.update(id, logDto);
expect(result).toEqual(logDto);
expect(mockLogRepository.findOneBy).toHaveBeenCalled();
expect(mockLogRepository.findOneBy).toHaveBeenCalledWith({ id: id });
expect(mockLogRepository.save).toHaveBeenCalled();
expect(mockLogRepository.save).toHaveBeenCalledWith(logDto);
})
})

View File

@@ -1,52 +1,34 @@
import { Injectable } from '@nestjs/common';
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 '../pilot/pilot.service';
import { PilotEntity } from '../pilot/pilot.entity';
import { CustomError } from '../error/customError';
import { FileService } from '../file/file.service';
import { InjectRepository, Repository } from '@noahspan/azure-database';
import { Log } from './log.entity';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class LogService {
constructor(
@InjectRepository(LogEntity) private readonly logRepository: Repository<LogEntity>,
private readonly fileService: FileService,
private readonly pilotService: PilotService,
@InjectRepository(Log) private readonly logRepository: Repository<Log>
) {}
async find(id: string): Promise<LogEntity> {
const logEntity: LogEntity = await this.logRepository.findOne({
where: { id: id },
relations: ['pilot', 'tracks']
});
return logEntity;
async find(partitionKey: string, rowKey: string): Promise<Log> {
return await this.logRepository.find(partitionKey, rowKey);
}
async findAll(): Promise<LogEntity[]> {
return await this.logRepository.find({
relations: ['pilot', 'tracks']
});
async findAll(): Promise<Log[]> {
return await this.logRepository.findAll();
}
async create(logDto: LogDto): Promise<LogDto> {
return this.logRepository.save(logDto);
async create(log: Log): Promise<Log> {
log.partitionKey = 'log';
log.rowKey = uuidv4();
return await this.logRepository.create(log);
}
async update(id: string, logDto: LogDto): Promise<LogDto> {
const logEntity: LogEntity = await this.logRepository.findOneBy({ id });
const logEntityUpdated = Object.assign(logEntity, logDto)
delete logEntityUpdated.tracks;
return await this.logRepository.save(logEntityUpdated);
async update(partitionKey: string, rowKey: string, log: Log): Promise<Log> {
return await this.logRepository.update(partitionKey, rowKey, log);
}
async delete(id: string): Promise<DeleteResult> {
await this.fileService.deleteFolder('tracks', id);
return await this.logRepository.delete({ id });
async delete(partitionKey: string, rowKey: string): Promise<void> {
await this.logRepository.delete(partitionKey, rowKey);
}
}

View File

@@ -3,7 +3,7 @@ import { AppModule } from './app.module';
import { HttpService } from '@nestjs/axios';
import { HttpExceptionFilter } from './filters/http-exception.filter';
import { InternalServerErrorException } from '@nestjs/common';
import * as session from 'express-session';
import { AuthGuard } from '@nestjs/passport';
async function bootstrap() {
const httpService = new HttpService();
@@ -12,13 +12,6 @@ async function bootstrap() {
app.enableCors();
app.setGlobalPrefix('api');
app.useGlobalFilters(new HttpExceptionFilter());
app.use(
session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false
})
)
httpService.axiosRef.interceptors.response.use(
(response) => {

View File

@@ -1,71 +0,0 @@
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

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

View File

@@ -1,18 +0,0 @@
import { PilotEntity } from '../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

@@ -1,33 +0,0 @@
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

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

View File

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

View File

@@ -0,0 +1,35 @@
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

@@ -1,260 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PilotController } from "./pilot.controller";
import { PilotService } from './pilot.service';
import { PilotDto } from './pilot.dto';
import { PilotEntity } from './pilot.entity';
import { CustomError } from '../error/customError';
import { HttpException } from '@nestjs/common';
describe('PilotController', () => {
let controller: PilotController;
const mockPilotService = {
create: jest.fn(),
delete: jest.fn(),
find: jest.fn(),
findAll: jest.fn(),
update: jest.fn(),
}
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [PilotController],
providers: [
{
provide: PilotService,
useValue: mockPilotService
}
]
}).compile();
controller = module.get<PilotController>(PilotController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
it('find => should find a pilot by id', async () => {
const id: string = '39465ae3-7947-4cc2-b565-ca00a982fdd8'
const pilot = {
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
name: 'Test pilot',
address: '123 Any Street',
city: 'Any Town',
state: 'Minnesota',
postalCode: '55123',
email: 'user@example.com',
phone: '555-555-1234',
userId: 'user@example.com',
logs: [],
certificates: [],
endorsements: [],
medical: []
} as PilotEntity;
jest.spyOn(mockPilotService, 'find').mockReturnValue(pilot);
const result = await controller.find(id);
expect(result).toEqual(pilot);
expect(mockPilotService.find).toHaveBeenCalled();
expect(mockPilotService.find).toHaveBeenCalledWith(id)
})
it('find => should fail to find a pilot by id', async () => {
const id: string = '39465ae3-7947-4cc2-b565-ca00a982fdd8'
jest.spyOn(mockPilotService, 'find').mockRejectedValue(new Error('Pilot not found'));
try {
await controller.find(id);
fail('find did not throw error')
} catch (error) {
expect(error).toBeInstanceOf(Error)
expect(mockPilotService.find).toHaveBeenCalled();
expect(mockPilotService.find).toHaveBeenCalledWith(id);
expect(mockPilotService.find).rejects.toThrow('Pilot not found')
}
})
it('findAll => should find all pilots', async () => {
const pilot = {
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
name: 'Test pilot',
address: '123 Any Street',
city: 'Any Town',
state: 'Minnesota',
postalCode: '55123',
email: 'user@example.com',
phone: '555-555-1234',
userId: 'user@example.com',
logs: [],
certificates: [],
endorsements: [],
medical: []
} as PilotEntity;
const pilots = [pilot];
jest.spyOn(mockPilotService, 'findAll').mockReturnValue(pilots);
const result = await controller.findAll();
expect(result).toEqual(pilots);
expect(mockPilotService.findAll).toHaveBeenCalled();
})
it('findAll => should fail to find all pilots', async () => {
jest.spyOn(mockPilotService, 'findAll').mockRejectedValue(new Error('Pilots not found'));
try {
await controller.findAll();
fail('findAll did not throw error')
} catch (error) {
expect(error).toBeInstanceOf(Error)
expect(mockPilotService.findAll).toHaveBeenCalled();
expect(mockPilotService.findAll).rejects.toThrow('Pilots not found')
}
})
it('create => should create a new pilot', async () => {
const pilotDto = {
name: 'Test pilot',
address: '123 Any Street',
city: 'Any Town',
state: 'Minnesota',
postalCode: '55123',
email: 'user@example.com',
phone: '555-555-1234'
} as PilotDto
const pilot = {
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
name: 'Test pilot',
address: '123 Any Street',
city: 'Any Town',
state: 'Minnesota',
postalCode: '55123',
email: 'user@example.com',
phone: '555-555-1234',
userId: 'user@example.com',
logs: [],
certificates: [],
endorsements: [],
medical: []
} as PilotEntity
jest.spyOn(mockPilotService, 'create').mockReturnValue(pilot);
const result = await controller.create(pilotDto);
expect(mockPilotService.create).toHaveBeenCalled();
expect(mockPilotService.create).toHaveBeenCalledWith(pilotDto)
expect(result).toEqual(pilot);
})
it('create => should fail to create a new pilot', async () => {
const pilotDto = {
name: 'Test pilot',
address: '123 Any Street',
city: 'Any Town',
state: 'Minnesota',
postalCode: '55123',
email: 'user@example.com',
phone: '555-555-1234'
} as PilotDto
jest.spyOn(mockPilotService, 'create').mockRejectedValue(new Error('Pilot failed to create'));
try {
await controller.create(pilotDto);
fail('create did not throw error')
} catch (error) {
expect(error).toBeInstanceOf(Error)
expect(mockPilotService.create).toHaveBeenCalled();
expect(mockPilotService.create).toHaveBeenCalledWith(pilotDto);
expect(mockPilotService.create).rejects.toThrow('Pilot failed to create')
}
})
it('update => should update an existing pilot', async () => {
const id: string = '39465ae3-7947-4cc2-b565-ca00a982fdd8'
const pilotDto = {
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
name: 'Test pilot',
address: '123 Any Street',
city: 'Any Town',
state: 'Minnesota',
postalCode: '55123',
email: 'user@example.com',
phone: '555-555-1234'
} as PilotDto
jest.spyOn(mockPilotService, 'update').mockReturnValue(pilotDto);
const result = await controller.update(id, pilotDto);
expect(result).toEqual(pilotDto);
expect(mockPilotService.update).toHaveBeenCalled();
expect(mockPilotService.update).toHaveBeenCalledWith(id, pilotDto)
})
it('update => should fail to update an existing pilot', async () => {
const id: string = '39465ae3-7947-4cc2-b565-ca00a982fdd8'
const pilotDto = {
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
name: 'Test pilot',
address: '123 Any Street',
city: 'Any Town',
state: 'Minnesota',
postalCode: '55123',
email: 'user@example.com',
phone: '555-555-1234'
} as PilotDto
jest.spyOn(mockPilotService, 'update').mockRejectedValue(new Error('Pilot failed to update'))
try {
await controller.update(id, pilotDto);
fail('update function did not throw error')
} catch (error) {
expect(error).toBeInstanceOf(Error)
expect(mockPilotService.update).toHaveBeenCalled();
expect(mockPilotService.update).toHaveBeenCalledWith(id, pilotDto);
expect(mockPilotService.update).rejects.toThrow('Pilot failed to update')
}
})
it('delete => should delete an existing pilot', async () => {
const id = '39465ae3-7947-4cc2-b565-ca00a982fdd8';
jest.spyOn(mockPilotService, 'delete');
const result = await controller.delete(id);
expect(mockPilotService.delete).toHaveBeenCalled();
expect(mockPilotService.delete).toHaveBeenCalledWith(id);
})
it('delete => should fail to delete an existing pilot', async () => {
const id = '39465ae3-7947-4cc2-b565-ca00a982fdd8';
jest.spyOn(mockPilotService, 'delete').mockRejectedValue(new Error('Pilot failed to delete'));
try {
await controller.delete(id);
fail('delete function did not throw error')
} catch (error) {
expect(error).toBeInstanceOf(Error)
expect(mockPilotService.delete).toHaveBeenCalled();
expect(mockPilotService.delete).toHaveBeenCalledWith(id);
expect(mockPilotService.delete).rejects.toThrow('Pilot failed to delete')
}
})
})

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 { PilotInterceptor } from './pilot.interceptor';
import { AuthGuard, Public } from '@noahspan/noahspan-modules';
import { Reflector } from '@nestjs/core';
const reflector = new Reflector();
import { AuthGuard } from '@noahspan/noahspan-modules'
import { PilotInterceptor } from './interceptors/pilot.interceptor';
@Controller('pilots')
@UseInterceptors(new PilotInterceptor(reflector))
@UseInterceptors(new PilotInterceptor())
export class PilotController {
constructor(private readonly pilotService: PilotService) {}
@Get(':id')
@Public()
async find(@Param('id') id: string) {
@Get(':partitionKey/:rowKey')
async find(
@Param('partitionKey') partitionKey: string,
@Param('rowKey') rowKey: string
) {
try {
return await this.pilotService.find(id);
return await this.pilotService.find(partitionKey, rowKey);
} catch (error) {
const customError = error as CustomError;
@@ -37,7 +37,6 @@ export class PilotController {
}
@Get()
@Public()
async findAll() {
try {
return await this.pilotService.findAll();
@@ -48,11 +47,30 @@ export class PilotController {
}
}
@Post()
@UseGuards(AuthGuard)
async create(@Body() pilotDto: PilotDto): Promise<PilotDto> {
@Post()
async create(@Body() pilotDto: PilotDto) {
try {
return await this.pilotService.create(pilotDto);
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);
} catch (error) {
const customError = error as CustomError;
@@ -60,14 +78,34 @@ export class PilotController {
}
}
@Put(':id')
@UseGuards(AuthGuard)
@Put(':partitionKey/:rowKey')
async update(
@Param('id') id: string,
@Param('partitionKey') partitionKey: string,
@Param('rowKey') rowKey: string,
@Body() pilotDto: PilotDto
) {
try {
return await this.pilotService.update(id, pilotDto);
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);
} catch (error) {
const customError = error as CustomError;
@@ -75,13 +113,14 @@ export class PilotController {
}
}
@Delete(':id')
@UseGuards(AuthGuard)
@Delete(':partitionKey/:rowKey')
async delete(
@Param('id') id: string,
@Param('partitionKey') partitionKey: string,
@Param('rowKey') rowKey: string
) {
try {
return await this.pilotService.delete(id);
return await this.pilotService.delete(partitionKey, rowKey);
} catch (error) {
const customError = error as CustomError;

View File

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

View File

@@ -1,47 +1,18 @@
import { LogEntity } from '../log/log.entity';
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
import { CertificateEntity } from '../certificate/certificate.entity';
import { EndorsementEntity } from '../endorsement/endorsement.entity';
import { MedicalEntity } from '../medical/medical.entity';
import { EntityString } from '@noahspan/azure-database';
@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[];
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;
}

View File

@@ -1,52 +0,0 @@
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
import { jwtDecode } from 'jwt-decode';
import { Observable, map } from 'rxjs';
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
};
})
}
if (req.headers.authorization) {
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1];
const jwtPayload = jwtDecode(token);
const rolesKeyName = Object.keys(jwtPayload).find((key) => key.includes('roles'));
if (jwtPayload[rolesKeyName].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,15 +1,41 @@
import { Module } from '@nestjs/common';
import { PilotController } from './pilot.controller';
import { PilotService } from './pilot.service';
import { PilotEntity } from './pilot.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AzureTableStorageModule } from '@noahspan/azure-database';
import { Pilot } from './pilot.entity';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { Log } from 'src/log/log.entity';
@Module({
imports: [
TypeOrmModule.forFeature([PilotEntity])
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'
})
],
controllers: [PilotController],
exports: [PilotService],
providers: [PilotService]
})
export class PilotModule {}

View File

@@ -1,206 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PilotService } from './pilot.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { PilotEntity } from './pilot.entity';
import { PilotDto } from './pilot.dto';
import { CustomError } from '../error/customError';
describe('PilotService', () => {
let service: PilotService;
const mockPilotRepository = {
delete: jest.fn(),
find: jest.fn(),
findOneBy: jest.fn(),
save: jest.fn(),
}
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
PilotService,
{
provide: getRepositoryToken(PilotEntity),
useValue: mockPilotRepository
}
]
}).compile();
service = module.get<PilotService>(PilotService);
})
it('should be defined', () => {
expect(service).toBeDefined();
})
it('find => should find one pilot by id', async () => {
const id: string = '39465ae3-7947-4cc2-b565-ca00a982fdd8'
const pilot = {
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
name: 'Test pilot',
address: '123 Any Street',
city: 'Any Town',
state: 'Minnesota',
postalCode: '55123',
email: 'user@example.com',
phone: '555-555-1234',
userId: 'user@example.com',
logs: [],
certificates: [],
endorsements: [],
medical: []
} as PilotEntity;
jest.spyOn(mockPilotRepository, 'findOneBy').mockReturnValue(pilot);
const result = await service.find(id);
expect(result).toEqual(pilot);
expect(mockPilotRepository.findOneBy).toHaveBeenCalled();
expect(mockPilotRepository.findOneBy).toHaveBeenCalledWith({ id: id });
})
it('find => should fail to find one pilot by id', async () => {
const id: string = '39465ae3-7947-4cc2-b565-ca00a982fdd8';
const mockCustomError = new CustomError('Pilot not found', 'Not found', 404)
jest.spyOn(mockPilotRepository, 'findOneBy').mockReturnValue(undefined);
try {
await service.find(id);
fail('find did not throw error')
} catch (error) {
expect(error).toBeInstanceOf(CustomError);
expect(mockPilotRepository.findOneBy).toHaveBeenCalled();
expect(mockPilotRepository.findOneBy).toHaveBeenCalledWith({ id: id });
}
})
it('findAll => should find all pilots', async () => {
const pilot = {
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
name: 'Test pilot',
address: '123 Any Street',
city: 'Any Town',
state: 'Minnesota',
postalCode: '55123',
email: 'user@example.com',
phone: '555-555-1234',
userId: 'user@example.com',
logs: [],
certificates: [],
endorsements: [],
medical: []
} as PilotEntity;
const pilots = [pilot];
jest.spyOn(mockPilotRepository, 'find').mockReturnValue(pilots);
const result = await service.findAll();
expect(result).toEqual(pilots);
expect(mockPilotRepository.find).toHaveBeenCalled()
})
it('create => should create a new pilot', async () => {
const pilotDto = {
name: 'Test pilot',
address: '123 Any Street',
city: 'Any Town',
state: 'Minnesota',
postalCode: '55123',
email: 'user@example.com',
phone: '555-555-1234'
} as PilotDto
const pilot = {
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
name: 'Test pilot',
address: '123 Any Street',
city: 'Any Town',
state: 'Minnesota',
postalCode: '55123',
email: 'user@example.com',
phone: '555-555-1234',
userId: 'user@example.com',
logs: [],
certificates: [],
endorsements: [],
medical: []
} as PilotEntity
jest.spyOn(mockPilotRepository, 'save').mockReturnValue(pilot);
const result = await service.create(pilotDto);
expect(mockPilotRepository.save).toHaveBeenCalled();
expect(mockPilotRepository.save).toHaveBeenCalledWith(pilotDto);
})
it('update => should update a pilot', async () => {
const id: string = '39465ae3-7947-4cc2-b565-ca00a982fdd8'
const pilotDto = {
name: 'Test pilot',
address: '123 Any Street',
city: 'Any Town',
state: 'Minnesota',
postalCode: '55123',
email: 'user@example.com',
phone: '555-555-1234'
} as PilotDto
const pilot = {
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
name: 'Test pilot',
address: '123 Any Street',
city: 'Any Town',
state: 'Minnesota',
postalCode: '55123',
email: 'user@example.com',
phone: '555-555-1234',
userId: 'user@example.com',
logs: [],
certificates: [],
endorsements: [],
medical: []
} as PilotEntity
jest.spyOn(mockPilotRepository, 'findOneBy').mockReturnValue(pilot)
jest.spyOn(mockPilotRepository, 'save').mockReturnValue(pilotDto);
const result = await service.update(id, pilotDto)
expect(result).toEqual(pilotDto);
expect(mockPilotRepository.findOneBy).toHaveBeenCalled();
expect(mockPilotRepository.findOneBy).toHaveBeenCalledWith({ id: id });
expect(mockPilotRepository.save).toHaveBeenCalled();
expect(mockPilotRepository.save).toHaveBeenCalledWith(pilotDto)
})
it('delete => should delete a pilot', async () => {
const id: string = '39465ae3-7947-4cc2-b565-ca00a982fdd8'
const pilot = {
id: '39465ae3-7947-4cc2-b565-ca00a982fdd8',
name: 'Test pilot',
address: '123 Any Street',
city: 'Any Town',
state: 'Minnesota',
postalCode: '55123',
email: 'user@example.com',
phone: '555-555-1234',
userId: 'user@example.com',
logs: [],
certificates: [],
endorsements: [],
medical: []
} as PilotEntity
jest.spyOn(mockPilotRepository, 'delete').mockReturnValue(pilot);
const result = await service.delete(id);
expect(result).toEqual(pilot);
expect(mockPilotRepository.delete).toHaveBeenCalled();
expect(mockPilotRepository.delete).toHaveBeenCalledWith({ id: id });
})
})

View File

@@ -1,49 +1,54 @@
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 '../error/customError';
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';
@Injectable()
export class PilotService {
constructor(
@InjectRepository(PilotEntity) private readonly pilotRepository: Repository<PilotEntity>
@InjectRepository(Pilot) private readonly pilotRepository: Repository<Pilot>,
@InjectRepository(Log) private readonly logRepository: Repository<Log>
) {}
async find(id: string): Promise<PilotEntity> {
try {
const pilotEntity: 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 find(partitionKey: string, rowKey: string): Promise<Pilot> {
return await this.pilotRepository.find(partitionKey, rowKey);
}
async findAll(): Promise<PilotEntity[]> {
return await this.pilotRepository.find();
async findAll(): Promise<Pilot[]> {
return await this.pilotRepository.findAll();
}
async create(pilot: PilotDto): Promise<PilotDto> {
return await this.pilotRepository.save(pilot);
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 update(
id: string,
pilotDto: PilotDto
): Promise<PilotDto> {
const pilotEntity: PilotEntity = await this.pilotRepository.findOneBy({ id })
const pilotEntityUpdated = Object.assign(pilotEntity, pilotDto)
return await this.pilotRepository.save(pilotEntityUpdated);
partitionKey: string,
rowKey: string,
pilot: Pilot
): Promise<Pilot> {
return await this.pilotRepository.update(partitionKey, rowKey, pilot);
}
async delete(id: string): Promise<DeleteResult> {
return await this.pilotRepository.delete({ id });
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
}
}

View File

@@ -1,218 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TrackController } from './track.controller';
import { TrackService } from './track.service';
import { TrackDto } from './track.dto';
import { TrackEntity } from './track.entity';
import { FileService } from '../file/file.service';
import { ConfigService } from '@nestjs/config';
import { HttpException } from '@nestjs/common';
import { Readable } from 'stream';
import { DeleteResult, InsertResult } from 'typeorm';
describe('TrackController', () => {
let controller: TrackController;
const mockTrackService = {
create: jest.fn(),
delete: jest.fn(),
downloadTrackFile: jest.fn(),
find: jest.fn(),
findAll: jest.fn(),
update: jest.fn()
}
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [TrackController],
providers: [
ConfigService,
FileService,
{
provide: TrackService,
useValue: mockTrackService
}
]
}).compile();
controller = module.get<TrackController>(TrackController);
})
it('should be defined', () => {
expect(controller).toBeDefined();
})
it('findAll => should find all tracks by log id', async () => {
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
const track = {
id: 'a6973d91-629f-4e10-aa28-016076a21fde',
url: 'http://track.example.com',
order: 1
} as TrackEntity
const tracks = [track]
jest.spyOn(mockTrackService, 'findAll').mockReturnValue(tracks);
const result = await controller.findAll(logId);
expect(result).toEqual(tracks);
expect(mockTrackService.findAll).toHaveBeenCalled();
expect(mockTrackService.findAll).toHaveBeenLastCalledWith(logId);
})
it('findAll => should fail to find all tracks by log id', async () => {
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
jest.spyOn(mockTrackService, 'findAll').mockRejectedValue(new Error('Tracks not found'))
try {
await controller.findAll(logId);
fail('findAll did not throw error');
} catch (error) {
expect(error).toBeInstanceOf(HttpException);
expect(mockTrackService.findAll).toHaveBeenCalled();
expect(mockTrackService.findAll).rejects.toThrow('Tracks not found')
}
})
it('create => create a track by a log id', async () => {
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
const order = 1;
const file: Express.Multer.File = {
fieldname: 'file',
originalname: 'test_track.kml',
encoding: 'utf-8',
mimetype: 'application/vnd.google-earth.kml+xml',
size: 12345,
destination: '/tmp/uploads',
filename: 'unique-filename-123.kml',
path: '/tmp/uploads/unique-filename-123.kml',
buffer: Buffer.from('<kml xmlns="http://www.opengis.net"></kml>'),
stream: new Readable()
}
const mockInsertResult: InsertResult = {
identifiers: [
{
id: 'a6973d91-629f-4e10-aa28-016076a21fde'
}
],
generatedMaps: [
{
id: 'a6973d91-629f-4e10-aa28-016076a21fde',
createdAd: new Date()
}
],
raw: {
affectedRows: 1
}
}
jest.spyOn(mockTrackService, 'create').mockReturnValue(mockInsertResult);
const result = await controller.create(logId, order, file);
expect(result).toEqual(mockInsertResult);
expect(mockTrackService.create).toHaveBeenCalled();
expect(mockTrackService.create).toHaveBeenCalledWith(logId, order, file)
})
it('create => should fail to create a track by log id', async () => {
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
const order = 1;
const file: Express.Multer.File = {
fieldname: 'file',
originalname: 'test_track.kml',
encoding: 'utf-8',
mimetype: 'application/vnd.google-earth.kml+xml',
size: 12345,
destination: '/tmp/uploads',
filename: 'unique-filename-123.kml',
path: '/tmp/uploads/unique-filename-123.kml',
buffer: Buffer.from('<kml xmlns="http://www.opengis.net"></kml>'),
stream: new Readable()
}
jest.spyOn(mockTrackService, 'create').mockRejectedValue(new Error('Track failed to create'))
try {
await controller.create(logId, order, file)
fail('create did not throw error')
} catch (error) {
expect(error).toBeInstanceOf(HttpException);
expect(mockTrackService.create).toHaveBeenCalled();
expect(mockTrackService.create).toHaveBeenCalledWith(logId, order, file);
expect(mockTrackService.create).rejects.toThrow('Track failed to create')
}
})
it('delete => should delete a track by log id', async () => {
const id = 'a6973d91-629f-4e10-aa28-016076a21fde'
const logId: string = '95834f84-0a02-44d3-884e-a20237adeca0';
const fileName = 'test_track.kml';
const mockDeleteResult: DeleteResult ={
raw: [],
affected: 1
}
jest.spyOn(mockTrackService, 'delete').mockReturnValue(mockDeleteResult);
const result = await controller.delete(id, fileName, logId);
expect(result).toEqual(mockDeleteResult);
expect(mockTrackService.delete).toHaveBeenCalled();
expect(mockTrackService.delete).toHaveBeenCalledWith(id, logId, fileName);
})
it('delete => should fail to delete a track by log id', async () => {
const id = 'a6973d91-629f-4e10-aa28-016076a21fde'
const logId: string = '95834f84-0a02-44d3-884e-a20237adeca0';
const fileName = 'test_track.kml';
jest.spyOn(mockTrackService, 'delete').mockRejectedValue(new Error('Track failed to delete'));
try {
await controller.delete(id, fileName, logId);
fail('delete did not throw error')
} catch (error) {
expect(error).toBeInstanceOf(HttpException);
expect(mockTrackService.delete).toHaveBeenCalled();
expect(mockTrackService.delete).toHaveBeenCalledWith(id, logId, fileName);
expect(mockTrackService.delete).rejects.toThrow('Track failed to delete')
}
})
it('downloadTrack => should download a track by log id', async () => {
const logId: string = '95834f84-0a02-44d3-884e-a20237adeca0';
const fileName = 'test_track.kml';
const mockStreamToBufferString = 'fake-stream-to-buffer-string'
jest.spyOn(mockTrackService, 'downloadTrackFile').mockReturnValue(mockStreamToBufferString);
const result = await controller.downloadTrack(logId, fileName);
expect(result).toEqual(mockStreamToBufferString);
expect(mockTrackService.downloadTrackFile).toHaveBeenCalled();
expect(mockTrackService.downloadTrackFile).toHaveBeenCalledWith(logId, fileName)
})
it('downloadTrack => should fail to download a track by log id', async () => {
const logId: string = '95834f84-0a02-44d3-884e-a20237adeca0';
const fileName = 'test_track.kml';
jest.spyOn(mockTrackService, 'downloadTrackFile').mockRejectedValue(new Error('Track failed to download'));
try {
await controller.downloadTrack(logId, fileName);
fail('downloadTrack did not throw error')
} catch (error) {
expect(error).toBeInstanceOf(HttpException);
expect(mockTrackService.downloadTrackFile).toHaveBeenCalled();
expect(mockTrackService.downloadTrackFile).toHaveBeenCalledWith(logId, fileName);
expect(mockTrackService.downloadTrackFile).rejects.toThrow('Track failed to download')
}
})
})

View File

@@ -1,80 +0,0 @@
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

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

View File

@@ -1,18 +0,0 @@
import { LogEntity } from '../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

@@ -1,20 +0,0 @@
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 '../log/log.module';
@Module({
imports: [
LogModule,
TypeOrmModule.forFeature([TrackEntity])
],
controllers: [TrackController],
providers: [
FileService,
TrackService
]
})
export class TrackModule {}

View File

@@ -1,346 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TrackService } from './track.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { TrackEntity } from './track.entity'
import { TrackDto } from './track.dto';
import { FileService } from '../file/file.service';
import { ConfigService } from '@nestjs/config';
import { LogService } from '../log/log.service';
import { LogEntity } from '../log/log.entity';
import { PilotService } from '../pilot/pilot.service';
import { PilotEntity } from '../pilot/pilot.entity';
import { Readable } from 'stream';
import { DeleteResult, InsertResult, UpdateResult } from 'typeorm';
import { CustomError } from '../error/customError';
describe('TrackService', () => {
let service: TrackService;
const mockFileService = {
deleteFile: jest.fn(),
downloadFile: jest.fn(),
uploadFile: jest.fn()
}
const mockLogService = {
create: jest.fn(),
delete: jest.fn(),
find: jest.fn(),
findAll: jest.fn(),
update: jest.fn()
}
const mockPilotRepository = {
create: jest.fn(),
delete: jest.fn(),
find: jest.fn(),
findAll: jest.fn(),
update: jest.fn()
}
const mockTrackRepository = {
create: jest.fn(),
delete: jest.fn(),
downloadTrackFile: jest.fn(),
findOneBy: jest.fn(),
find: jest.fn(),
insert: jest.fn(),
update: jest.fn()
}
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ConfigService,
LogService,
PilotService,
TrackService,
{
provide: FileService,
useValue: mockFileService
},
{
provide: LogService,
useValue: mockLogService
},
{
provide: getRepositoryToken(PilotEntity),
useValue: mockPilotRepository
},
{
provide: getRepositoryToken(TrackEntity),
useValue: mockTrackRepository
}
]
}).compile()
service = module.get<TrackService>(TrackService);
})
it('should be defined', () => {
expect(service).toBeDefined();
})
it('create => should upload a track file', async () => {
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
const log = {
id: '094ec69c-72c4-4995-8821-79d5b79bedda',
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
date: new Date('2026-02-21'),
aircraftMakeModel: 'Cessna 172M',
aircraftIdentity: 'N12345',
routeFrom: 'KMSP',
routeTo: 'KMSP',
durationOfFlight: 1,
singleEngineLand: 1,
simulatorAtd: null,
landingsDay: 1,
landingsNight: null,
groundTrainingReceived: null,
flightTrainingReceived: null,
crossCountry: 1,
night: null,
solo: 1,
pilotInCommand: 1,
instrumentActual: null,
instrumentSimulated: null,
instrumentApproaches: null,
instrumentHolds: null,
instrumentNavTrack: null,
notes: 'This is a test',
tracks: []
} as LogEntity;
const order = 1;
const file: Express.Multer.File = {
fieldname: 'file',
originalname: 'test_track.kml',
encoding: 'utf-8',
mimetype: 'application/vnd.google-earth.kml+xml',
size: 12345,
destination: '/tmp/uploads',
filename: 'unique-filename-123.kml',
path: '/tmp/uploads/unique-filename-123.kml',
buffer: Buffer.from('<kml xmlns="http://www.opengis.net"></kml>'),
stream: new Readable()
}
const mockInsertResult: InsertResult = {
identifiers: [
{
id: 'a6973d91-629f-4e10-aa28-016076a21fde'
}
],
generatedMaps: [
{
id: 'a6973d91-629f-4e10-aa28-016076a21fde',
createdAd: new Date()
}
],
raw: {
affectedRows: 1
}
}
const track = {
id: 'a6973d91-629f-4e10-aa28-016076a21fde',
url: 'http://track.example.com',
order: 1
} as TrackEntity
jest.spyOn(mockLogService, 'find').mockReturnValue(log)
jest.spyOn(mockTrackRepository, 'create').mockReturnValue(track)
jest.spyOn(mockTrackRepository, 'insert').mockReturnValue(mockInsertResult);
const result = await service.create(logId, order, file);
expect(result).toEqual(mockInsertResult)
expect(mockTrackRepository.insert).toHaveBeenCalled();
expect(mockTrackRepository.insert).toHaveBeenCalledWith({
id: track.id,
order: track.order,
url: track.url
})
})
it('create => should fail to find log', async () => {
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
const order = 1;
const file: Express.Multer.File = {
fieldname: 'file',
originalname: 'test_track.kml',
encoding: 'utf-8',
mimetype: 'application/vnd.google-earth.kml+xml',
size: 12345,
destination: '/tmp/uploads',
filename: 'unique-filename-123.kml',
path: '/tmp/uploads/unique-filename-123.kml',
buffer: Buffer.from('<kml xmlns="http://www.opengis.net"></kml>'),
stream: new Readable()
}
jest.spyOn(mockLogService, 'find').mockReturnValue(undefined);
try {
await service.create(logId, order, file);
fail('find failed to throw error')
} catch (error) {
expect(error).toBeInstanceOf(CustomError);
expect(mockLogService.find).toHaveBeenCalled();
expect(mockLogService.find).toHaveBeenCalledWith(logId);
}
})
it('delete => should delete a track file', async () => {
const id = 'a6973d91-629f-4e10-aa28-016076a21fde';
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
const fileName = 'test_track.kml'
const mockDeleteResult: DeleteResult ={
raw: [],
affected: 1
}
jest.spyOn(mockTrackRepository, 'delete').mockReturnValue(mockDeleteResult);
const result = await service.delete(id, logId, fileName)
expect(result).toEqual(mockDeleteResult);
expect(mockTrackRepository.delete).toHaveBeenCalled();
expect(mockTrackRepository.delete).toHaveBeenCalledWith({ id: id })
})
it('delete => should fail to delete file in file service', async () => {
const id = 'a6973d91-629f-4e10-aa28-016076a21fde';
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
const fileName = 'test_track.kml'
jest.spyOn(mockFileService, 'deleteFile').mockRejectedValue(new Error('Failed to delete file'))
try {
await service.delete(id, logId, fileName)
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect(mockFileService.deleteFile).toHaveBeenCalled();
expect(mockFileService.deleteFile).toHaveBeenCalledWith('tracks', logId, fileName)
expect(mockFileService.deleteFile).rejects.toThrow('Failed to delete file')
}
})
it('downloadTrackFile => should download a track file by log id and filename', async () => {
const logId: string = '95834f84-0a02-44d3-884e-a20237adeca0';
const fileName = 'test_track.kml';
const mockStreamToBufferString = 'fake-stream-to-buffer-string'
jest.spyOn(mockFileService, 'downloadFile').mockReturnValue(mockStreamToBufferString);
const result = await service.downloadTrackFile(logId, fileName);
expect(result).toEqual(mockStreamToBufferString);
expect(mockFileService.downloadFile).toHaveBeenCalled();
expect(mockFileService.downloadFile).toHaveBeenCalledWith('tracks', logId, fileName);
})
it('find => should find a track file by id', async () => {
const id = 'a6973d91-629f-4e10-aa28-016076a21fde';
const track = {
id: 'a6973d91-629f-4e10-aa28-016076a21fde',
url: 'http://track.example.com',
order: 1
} as TrackEntity
jest.spyOn(mockTrackRepository, 'findOneBy').mockReturnValue(track)
const result = await service.find(id);
expect(result).toEqual(track);
expect(mockTrackRepository.findOneBy).toHaveBeenCalled();
expect(mockTrackRepository.findOneBy).toHaveBeenCalledWith({ id: id })
})
it('findAll => should find all track files by log id', async () => {
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
const log = {
id: '094ec69c-72c4-4995-8821-79d5b79bedda',
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
date: new Date('2026-02-21'),
aircraftMakeModel: 'Cessna 172M',
aircraftIdentity: 'N12345',
routeFrom: 'KMSP',
routeTo: 'KMSP',
durationOfFlight: 1,
singleEngineLand: 1,
simulatorAtd: null,
landingsDay: 1,
landingsNight: null,
groundTrainingReceived: null,
flightTrainingReceived: null,
crossCountry: 1,
night: null,
solo: 1,
pilotInCommand: 1,
instrumentActual: null,
instrumentSimulated: null,
instrumentApproaches: null,
instrumentHolds: null,
instrumentNavTrack: null,
notes: 'This is a test',
tracks: []
} as LogEntity;
const track = {
id: 'a6973d91-629f-4e10-aa28-016076a21fde',
url: 'http://track.example.com',
order: 1
} as TrackEntity;
const tracks = [track]
jest.spyOn(mockLogService, 'find').mockReturnValue(log)
jest.spyOn(mockTrackRepository, 'find').mockReturnValue(tracks);
const result = await service.findAll(logId);
expect(result).toEqual(tracks);
expect(mockTrackRepository.find).toHaveBeenCalled();
expect(mockTrackRepository.find).toHaveBeenCalledWith(
{
where: {
log: {
id: logId
}
}
})
})
it('findAll => should fail to find log', async () => {
const logId = '094ec69c-72c4-4995-8821-79d5b79bedda';
jest.spyOn(mockLogService, 'find').mockReturnValue(undefined);
try {
await service.findAll(logId);
fail('find failed to throw error')
} catch (error) {
expect(error).toBeInstanceOf(CustomError);
expect(mockLogService.find).toHaveBeenCalled();
expect(mockLogService.find).toHaveBeenCalledWith(logId);
}
})
it('update => should update a track', async () => {
const id = 'a6973d91-629f-4e10-aa28-016076a21fde';
const track = {
} as TrackDto;
const mockUpdateResult: UpdateResult = {
affected: 1,
raw: [],
generatedMaps: []
}
jest.spyOn(mockTrackRepository, 'update').mockReturnValue(mockUpdateResult)
const result = await service.update(id, track);
expect(result).toEqual(mockUpdateResult);
expect(mockTrackRepository.update).toHaveBeenCalled();
expect(mockTrackRepository.update).toHaveBeenCalledWith(id, track)
})
})

View File

@@ -1,85 +0,0 @@
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 "../log/log.service";
import { LogEntity } from "../log/log.entity";
import { CustomError } from "../error/customError";
import { FileService } from "../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> {
return await this.trackRepository.findOneBy({ id });
}
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;
} else {
throw new CustomError('Tracks not found', 'Not found', 404);
}
} catch (error) {
throw error
}
}
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> {
return await this.trackRepository.update(id, track);
}
async downloadTrackFile(logId: string, fileName: string): Promise<string> {
return await this.fileService.downloadFile(this.containerName, logId, fileName);
}
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
}
}
}

View File

@@ -1,9 +0,0 @@
export class ColumnNumericTransformer {
to(data: number): number {
return data;
}
from (data: string): number {
return parseFloat(data);
}
}

View File

View File

@@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/noahspan-logo.png" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Flying</title>
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>

View File

@@ -1,7 +1,7 @@
{
"name": "client",
"name": "app",
"private": true,
"version": "2.0.0",
"version": "1.3.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -11,31 +11,25 @@
"serve": "serve -s dist"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^7.1.0",
"@fortawesome/free-solid-svg-icons": "^7.1.0",
"@fortawesome/react-fontawesome": "^3.1.0",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.13",
"@tanstack/react-table": "^8.21.3",
"@azure/msal-browser": "^4.0.1",
"@azure/msal-react": "^3.0.1",
"@noahspan/noahspan-components": "1.6.0",
"axios": "^1.7.2",
"dotenv": "^16.4.7",
"leaflet": "^1.9.4",
"oidc-spa": "^7.2.4",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react": "19.0.0-rc.1",
"react-dom": "19.0.0-rc.1",
"react-hook-form": "^7.51.4",
"react-leaflet": "^4",
"react-leaflet": "^5.0.0",
"react-leaflet-kml": "^2.1.2",
"react-oidc-context": "^3.3.0",
"react-router-dom": "^6.23.0",
"swiper": "^11.2.6",
"tailwindcss": "^4.1.13"
"swiper": "^11.2.6"
},
"devDependencies": {
"@microsoft/microsoft-graph-types": "^2.40.0",
"@types/leaflet": "^1.9.16",
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.8",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@vitejs/plugin-react": "^4.2.1",
"typescript": "^5.2.2",
"vite": "^5.2.0"

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 696 B

After

Width:  |  Height:  |  Size: 696 B

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 618 B

After

Width:  |  Height:  |  Size: 618 B

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,22 +1,31 @@
import { Route, Routes } from 'react-router-dom';
import { Navigate, Route, Routes } from 'react-router-dom';
import Flights from './components/flights/Flights';
import Logbook from './components/logbook/Logbook';
import Pilots from './components/pilots/Pilots';
import SiteNav from './components/siteNav/SiteNav';
import { useNavigate } from 'react-router-dom';
import { useIsAuthenticated } from '@azure/msal-react';
interface ProtectedRouteProps {
children: React.ReactNode;
}
const App = () => {
const navigate = useNavigate();
const isAuthenticated = useIsAuthenticated()
const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
return isAuthenticated ? children : <Navigate to='/' />
}
return (
<div className='bg-[#f5f5f5]' data-theme="lofi">
<>
<SiteNav />
<Routes>
<Route path='/' element={<Flights />} />
<Route path="/logbook" element={<Logbook />} />
<Route path="/pilots" element={<Pilots />} />
</Routes>
</div>
</>
);
};

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,70 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { LogLevel } from '@azure/msal-browser';
/**
* Configuration object to be passed to MSAL instance on creation.
* For a full list of MSAL.js configuration parameters, visit:
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md
*/
export const msalConfig = {
auth: {
clientId: import.meta.env.VITE_CLIENT_ID, // This is the ONLY mandatory field that you need to supply.
authority: `https://login.microsoftonline.com/${import.meta.env.VITE_TENANT_ID}`, // Replace the placeholder with your tenant subdomain
redirectUri: import.meta.env.VITE_REDIRECT_URL, // Points to window.location.origin. You must register this URI on Microsoft Entra admin center/App Registration.
postLogoutRedirectUri: '/', // Indicates the page to navigate after logout.
navigateToLoginRequestUrl: false, // If "true", will navigate back to the original request location before processing the auth code response.
},
cache: {
cacheLocation: 'sessionStorage', // Configures cache location. "sessionStorage" is more secure, but "localStorage" gives you SSO between tabs.
storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
},
system: {
loggerOptions: {
loggerCallback: (level: any, message: any, containsPii: any) => {
if (containsPii) {
return;
}
switch (level) {
case LogLevel.Error:
console.error(message);
return;
case LogLevel.Info:
console.info(message);
return;
case LogLevel.Verbose:
console.debug(message);
return;
case LogLevel.Warning:
console.warn(message);
return;
default:
return;
}
},
},
},
};
/**
* Scopes you add here will be prompted for user consent during sign-in.
* By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request.
* For more information about OIDC scopes, visit:
* https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
*/
export const loginRequest = {
scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`],
};
/**
* An optional silentRequest object can be used to achieve silent SSO
* between applications by providing a "login_hint" property.
*/
// export const silentRequest = {
// scopes: ["openid", "profile"],
// loginHint: "example@domain.net"
// };

View File

@@ -0,0 +1,81 @@
import { useState } from 'react';
import { IActionMenuProps } from './IActionMenuProps';
import {
IconButton,
Icon,
IconName,
ListItemIcon,
ListItemText,
Menu,
MenuItem
} from '@noahspan/noahspan-components';
import { FormMode } from '../../enums/formMode';
import { useIsAuthenticated } from '@azure/msal-react';
const ActionMenu = ({ id, onDelete, onOpenCloseForm, onOpenCloseTracks }: IActionMenuProps) => {
const [anchorElAction, setAnchorElAction] = useState<null | HTMLElement>(
null
);
const isAuthenticated = useIsAuthenticated();
const onOpenActionMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorElAction(event.currentTarget);
};
const onCloseActionMenu = () => {
setAnchorElAction(null);
};
return (
<div>
<IconButton onClick={onOpenActionMenu}>
<Icon iconName={IconName.ELLIPSIS_VERTICAL} size="sm" />
</IconButton>
<Menu
anchorEl={anchorElAction}
keepMounted
open={Boolean(anchorElAction)}
onClose={onCloseActionMenu}
>
{isAuthenticated &&
<>
<MenuItem onClick={() => onOpenCloseForm(FormMode.EDIT, id)}>
<ListItemIcon>
<Icon iconName={IconName.PEN} size="lg" />
</ListItemIcon>
<ListItemText>Edit</ListItemText>
</MenuItem>
{onOpenCloseTracks &&
<MenuItem onClick={() => onOpenCloseTracks!(FormMode.EDIT, id)}>
<ListItemIcon>
<Icon iconName={IconName.MAP_LOCATION_DOT} size="lg" />
</ListItemIcon>
<ListItemText>Tracks</ListItemText>
</MenuItem>
}
</>
}
<MenuItem onClick={() => onOpenCloseForm(FormMode.VIEW, id)}>
<ListItemIcon>
<Icon iconName={IconName.EYE} size="lg" />
</ListItemIcon>
<ListItemText>View</ListItemText>
</MenuItem>
{isAuthenticated &&
<>
<hr className="my-3" />
<MenuItem onClick={() => onDelete(id)}>
<ListItemIcon>
<Icon iconName={IconName.TRASH} size="lg" />
</ListItemIcon>
<ListItemText>Delete</ListItemText>
</MenuItem>
</>
}
</Menu>
</div>
);
};
export default ActionMenu;

View File

@@ -0,0 +1,8 @@
import { FormMode } from '../../enums/formMode';
export interface IActionMenuProps {
id: string;
onDelete: (entryId: string) => void;
onOpenCloseForm: (formMode: FormMode, id: string) => void;
onOpenCloseTracks?: (formMode: FormMode, id: string) => void;
}

View File

@@ -0,0 +1,52 @@
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Icon,
IconName,
Spinner
} from '@noahspan/noahspan-components';
import { IDialogConfirmationProps } from './IConfirmationDialogProps';
const ConfirmationDialog = ({
contentText,
isLoading,
isOpen,
onCancel,
onConfirm,
title
}: IDialogConfirmationProps) => {
return (
<Dialog
onClose={onCancel}
open={isOpen}
sx={{
'& .MuiDialog-paper': { width: '2000px' }
}}
>
<DialogTitle>{title}</DialogTitle>
<DialogContent sx={{ textAlign: 'center' }}>
{!isLoading && <DialogContentText>{contentText}</DialogContentText>}
{isLoading && <Spinner />}
</DialogContent>
<DialogActions>
<Button onClick={onCancel} variant="outlined" startIcon={<Icon iconName={IconName.XMARK} />}>
No
</Button>
<Button
onClick={onConfirm}
variant="contained"
startIcon={<Icon iconName={IconName.CIRCLE_CHECK} />}
>
Yes
</Button>
</DialogActions>
</Dialog>
);
};
export default ConfirmationDialog;

View File

@@ -1,4 +1,4 @@
export interface DialogConfirmationProps {
export interface IDialogConfirmationProps {
contentText: string;
isLoading: boolean;
isOpen: boolean;

View File

@@ -0,0 +1,48 @@
import { Box, Container, Grid, Spinner } from "@noahspan/noahspan-components";
import LogbookCard from "../logbookCard/LogbookCard";
import { useEffect, useState } from "react";
import { useLogs } from "../../hooks/logs/UseLogs";
import { ILogbookEntry } from "../logbook/ILogbookEntry";
const Flights = () => {
const [flights, setFlights] = useState<ILogbookEntry[]>([]);
const { logs, isLoading } = useLogs();
useEffect(() => {
const flights: ILogbookEntry[] | undefined = logs?.filter((log: ILogbookEntry) => {
if (log.tracks && JSON.parse(log.tracks).length > 0) {
return log;
}
})
if (flights && flights.length > 0) {
setFlights(flights)
}
}, [logs])
return (
<Container>
<Box sx={{ margin: '20px' }}>
<Grid container spacing={2}>
{isLoading &&
<>
<Grid display="flex" justifyContent="center" size={12}>
<Spinner />
</Grid>
<Grid display="flex" justifyContent="center" size={12}>
Loading...
</Grid>
</>
}
{!isLoading &&
<Grid size={12}>
<LogbookCard logs={flights} mode='flights' />
</Grid>
}
</Grid>
</Box>
</Container>
)
}
export default Flights;

View File

@@ -0,0 +1,8 @@
import { FormMode } from '../../enums/formMode';
export interface ILogFormProps {
entryId?: string;
isDrawerOpen: boolean;
mode: FormMode;
onOpenClose: (mode: FormMode) => void;
}

View File

@@ -1,9 +1,9 @@
import { Alert } from "../../interfaces/Alert.interface";
export interface LogFormState {
export interface ILogFormState {
alert: Alert | undefined;
isDisabled: boolean;
isLoading: boolean;
pilotOptions: { label: string; value: string; }[];
selectedPilotName: string;
pilotOptions: { label: string; value: string }[];
selectedEntryPilotName: string;
}

View File

@@ -0,0 +1,947 @@
import React, { useEffect, useReducer } from 'react';
import {
Accordion,
AccordionDetails,
AccordionSummary,
Alert,
Button,
DatePicker,
Drawer,
Grid,
Icon,
IconButton,
IconName,
Select,
TextField,
theme,
Typography,
useMediaQuery
} from '@noahspan/noahspan-components';
import { useForm, Controller, FormProvider } from 'react-hook-form';
import { ILogFormProps } from './ILogFormProps';
import { initialState, reducer } from './reducer';
import { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
import { useIsAuthenticated } from '@azure/msal-react';
import { FormMode } from '../../enums/formMode';
import { usePilots } from '../../hooks/pilots/UsePilots';
const LogForm: React.FC<ILogFormProps> = ({
entryId,
isDrawerOpen,
mode,
onOpenClose
}) => {
const [state, dispatch] = useReducer(reducer, initialState);
const httpClient: AxiosInstance = useHttpClient();
const { getAccessToken } = useAccessToken();
const isAuthenticated = useIsAuthenticated();
const defaultValues = {
pilotId: '',
pilotName: '',
date: null,
aircraftMakeModel: '',
aircraftIdentity: '',
routeFrom: '',
routeTo: '',
durationOfFlight: null,
singleEngineLand: null,
simulatorAtd: null,
landingsDay: null,
landingsNight: null,
groundTrainingReceived: null,
flightTrainingReceived: null,
crossCountry: null,
night: null,
solo: null,
pilotInCommand: null,
instrumentActual: null,
instrumentSimulated: null,
instrumentApproaches: null,
instrumentHolds: null,
instrumentNavTrack: null,
notes: ''
};
const methods = useForm();
const { pilots } = usePilots();
const isMedium = useMediaQuery(theme.breakpoints.up('md'));
const onCancel = () => {
methods.reset(defaultValues);
dispatch({ type: 'SET_IS_DISABLED', payload: false });
onOpenClose(FormMode.CANCEL);
};
const onSubmit = async (data: unknown) => {
try {
dispatch({ type: 'SET_IS_LOADING', payload: true });
const accessToken: string = await getAccessToken();
if (!entryId) {
await httpClient.post(`api/logs`, data, {
headers: {
Authorization: accessToken
}
});
} else {
await httpClient.put(`api/logs/log/${entryId}`, data, {
headers: {
Authorization: accessToken
}
});
}
methods.reset(defaultValues);
dispatch({ type: 'SET_IS_DISABLED', payload: false });
onOpenClose(FormMode.CANCEL);
} catch (error) {
const axiosError = error as AxiosError;
dispatch({ type: 'SET_ALERT', payload: { severity: 'error', message: axiosError.message }});
} finally {
dispatch({ type: 'SET_IS_LOADING', payload: false });
}
};
useEffect(() => {
if (mode === FormMode.VIEW) {
dispatch({ type: 'SET_IS_DISABLED', payload: true });
}
}, [mode]);
useEffect(() => {
const getEntry = async () => {
try {
dispatch({ type: 'SET_IS_LOADING', payload: true });
const config = isAuthenticated
? { headers: { Authorization: await getAccessToken() } }
: {};
const response: AxiosResponse = await httpClient.get(
`api/logs/log/${entryId}`,
config
);
const entry = response.data;
// if (mode !== FormMode.ADD) {
// const pilot = pilots?.find((pilot) => pilot.id === entry.pilotId);
// console.log(pilot.name)
// dispatch({
// type: 'SET_SELECTED_ENTRY_PILOT_NAME',
// payload: pilot.name
// });
// }
methods.reset(entry);
} catch (error) {
const axiosError = error as AxiosError;
dispatch({ type: 'SET_ALERT', payload: { severity: 'error', message: axiosError.message }});
} finally {
dispatch({ type: 'SET_IS_LOADING', payload: false });
}
};
if (entryId && isDrawerOpen) {
getEntry();
}
}, [entryId]);
useEffect(() => {
if (pilots && FormMode.ADD) {
const newPilotsOptions = pilots.map((pilot) => {
return {
label: pilot.name,
value: pilot.id
};
});
dispatch({ type: 'SET_PILOT_OPTIONS', payload: newPilotsOptions });
}
}, [pilots]);
return (
<Drawer
open={isDrawerOpen}
anchor="right"
PaperProps={{
sx: {
padding: '30px',
width: isMedium ? '33%' : '75%'
}
}}
>
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)} style={{ paddingBottom: '50px' }}>
<Grid container spacing={2}>
<Grid size={11}>
<Typography variant="h4">{`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Entry`}</Typography>
</Grid>
<Grid display="flex" justifyContent="right" size={1}>
<IconButton onClick={onCancel}>
<Icon iconName={IconName.XMARK} />
</IconButton>
</Grid>
{state.alert && (
<Grid display="flex" justifyContent="center" size={12}>
<Alert
onClose={() =>
dispatch({ type: 'SET_ALERT', payload: undefined })
}
severity={state.alert.severity}
sx={{ width: '100%' }}
>
{state.alert.message}
</Alert>
</Grid>
)}
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
<Typography variant="body1">Pilot *</Typography>
</Grid>
<Grid size={isMedium ? 8 : 12}>
<Controller
name="pilotId"
control={methods.control}
render={({ field: { onChange, value } }) => {
return (
<Select
disabled={state.isDisabled}
fullWidth
onChange={(event: any) => {
const pilot = pilots?.find(
(pilot) => (pilot.id = event.target.value)
);
if (pilot) {
methods.setValue('pilotName', pilot.name);
}
methods.setValue('pilotId', event.target.value);
}}
options={
state.pilotOptions && state.pilotOptions.length > 0
? state.pilotOptions
: []
}
value={value ? value : ''}
/>
);
}}
/>
</Grid>
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
<Typography variant="body1">Date *</Typography>
</Grid>
<Grid size={isMedium ? 8 : 12}>
<Controller
name="date"
control={methods.control}
render={({ field: { onChange, value } }) => (
<DatePicker
disabled={state.isDisabled}
onChange={onChange}
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
<Typography variant="body1">Aircraft Make and Model *</Typography>
</Grid>
<Grid size={isMedium ? 8 : 12}>
<Controller
name="aircraftMakeModel"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
disabled={state.isDisabled}
error={methods.formState.errors.address ? true : false}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
value={value}
/>
)}
/>
</Grid>
{isAuthenticated &&
<>
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
<Typography variant="body1">Aircraft Identity *</Typography>
</Grid>
<Grid size={isMedium ? 8 : 12}>
<Controller
name="aircraftIdentity"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
disabled={state.isDisabled}
error={methods.formState.errors.address ? true : false}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
value={value}
/>
)}
/>
</Grid>
</>
}
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
<Typography variant="body1">Route From</Typography>
</Grid>
<Grid size={isMedium ? 8 : 12}>
<Controller
name="routeFrom"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
disabled={state.isDisabled}
error={methods.formState.errors.address ? true : false}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
<Typography variant="body1">Route To</Typography>
</Grid>
<Grid size={isMedium ? 8 : 12}>
<Controller
name="routeTo"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
disabled={state.isDisabled}
error={methods.formState.errors.address ? true : false}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
<Typography variant="body1">Duration Of Flight</Typography>
</Grid>
<Grid size={isMedium ? 8 : 12}>
<Controller
name="durationOfFlight"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
disabled={state.isDisabled}
error={methods.formState.errors.address ? true : false}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 0.1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
{isAuthenticated &&
<>
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
<Typography variant="body1">Single Engine Land</Typography>
</Grid>
<Grid size={isMedium ? 8 : 12}>
<Controller
name="singleEngineLand"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
disabled={state.isDisabled}
error={methods.formState.errors.address ? true : false}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 0.1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
</>
}
{isAuthenticated &&
<>
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
<Typography variant="body1">Simulator or ATD</Typography>
</Grid>
<Grid size={isMedium ? 8 : 12}>
<Controller
name="simulatorAtd"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
disabled={state.isDisabled}
error={methods.formState.errors.address ? true : false}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 0.1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
</>
}
{isAuthenticated &&
<Grid size={12}>
<Accordion defaultExpanded>
<AccordionSummary expandIcon={<Icon iconName={IconName.CHEVRON_DOWN} />}>
<Typography variant="body1">Landings</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={2}>
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
<Typography variant="body1">Day</Typography>
</Grid>
<Grid size={isMedium ? 8 : 12}>
<Controller
name="landingsDay"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
disabled={state.isDisabled}
error={
methods.formState.errors.address ? true : false
}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
<Typography variant="body1">Night</Typography>
</Grid>
<Grid size={isMedium ? 8 : 12}>
<Controller
name="landingsNight"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
disabled={state.isDisabled}
error={
methods.formState.errors.address ? true : false
}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
</Grid>
}
{isAuthenticated &&
<Grid size={12}>
<Accordion>
<AccordionSummary expandIcon={<Icon iconName={IconName.CHEVRON_DOWN} />}>
Instrument
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={2}>
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
<Typography variant="body1">Actual</Typography>
</Grid>
<Grid size={isMedium ? 8 : 12}>
<Controller
name="instrumentActual"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
disabled={state.isDisabled}
error={
methods.formState.errors.address ? true : false
}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 0.1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
<Typography variant="body1">Simulated</Typography>
</Grid>
<Grid size={isMedium ? 8 : 12}>
<Controller
name="instrumentSimulated"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
disabled={state.isDisabled}
error={
methods.formState.errors.address ? true : false
}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 0.1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
<Typography variant="body1">
Instrument Approaches
</Typography>
</Grid>
<Grid size={isMedium ? 8 : 12}>
<Controller
name="instrumentApproaches"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
disabled={state.isDisabled}
error={
methods.formState.errors.address ? true : false
}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={4}>
<Typography variant="body1">Holds</Typography>
</Grid>
<Grid size={isMedium ? 8 : 12}>
<Controller
name="instrumentHolds"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
disabled={state.isDisabled}
error={
methods.formState.errors.address ? true : false
}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
<Typography variant="body1">Nav / Track</Typography>
</Grid>
<Grid size={isMedium ? 8 : 12}>
<Controller
name="instrumentNavTrack"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
disabled={state.isDisabled}
error={
methods.formState.errors.address ? true : false
}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
</Grid>
}
{isAuthenticated &&
<Grid size={12}>
<Accordion defaultExpanded>
<AccordionSummary expandIcon={<Icon iconName={IconName.CHEVRON_DOWN} />}>
Type of Pilot Experience or Training
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={2}>
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
<Typography variant="body1">
Ground Training Received
</Typography>
</Grid>
<Grid size={isMedium ? 8 : 12}>
<Controller
name="groundTrainingReceived"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
disabled={state.isDisabled}
error={
methods.formState.errors.address ? true : false
}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 0.1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
<Typography variant="body1">
Flight Training Received
</Typography>
</Grid>
<Grid size={isMedium ? 8 : 12}>
<Controller
name="flightTrainingReceived"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
disabled={state.isDisabled}
error={
methods.formState.errors.address ? true : false
}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 0.1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
<Typography variant="body1">Cross Country</Typography>
</Grid>
<Grid size={isMedium ? 8 : 12}>
<Controller
name="crossCountry"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
disabled={state.isDisabled}
error={
methods.formState.errors.address ? true : false
}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 0.1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
<Typography variant="body1">Night</Typography>
</Grid>
<Grid size={isMedium ? 8 : 12}>
<Controller
name="night"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
disabled={state.isDisabled}
error={
methods.formState.errors.address ? true : false
}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 0.1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
<Typography variant="body1">Solo</Typography>
</Grid>
<Grid size={isMedium ? 8 : 12}>
<Controller
name="solo"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
disabled={state.isDisabled}
error={
methods.formState.errors.address ? true : false
}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 0.1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
<Typography variant="body1">Pilot in Command</Typography>
</Grid>
<Grid size={isMedium ? 8 : 12}>
<Controller
name="pilotInCommand"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
disabled={state.isDisabled}
error={
methods.formState.errors.address ? true : false
}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
slotProps={{
htmlInput: {
step: 0.1
}
}}
type="number"
value={value}
/>
)}
/>
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
</Grid>
}
<Grid size={isMedium ? 4 : 12}>
<Typography variant="body1">Notes</Typography>
</Grid>
<Grid size={isMedium ? 8 : 12}>
<Controller
name="notes"
control={methods.control}
render={({ field: { onChange, value } }) => (
<TextField
disabled={state.isDisabled}
error={methods.formState.errors.address ? true : false}
fullWidth
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
multiline
rows={3}
onChange={onChange}
value={value}
/>
)}
/>
</Grid>
<Grid display="flex" gap={2} justifyContent="right" size={12}>
<Button
disabled={
state.isDisabled && mode.toString() !== FormMode.VIEW
? state.isDisabled
: false
}
startIcon={<Icon iconName={IconName.XMARK} />}
variant="outlined"
onClick={onCancel}
size="small"
>
{mode.toString() !== FormMode.VIEW ? 'Cancel' : 'Close'}
</Button>
{mode.toString() !== FormMode.VIEW && (
<Button
disabled={state.isDisabled}
startIcon={<Icon iconName={IconName.SAVE} />}
size="small"
type="submit"
variant="contained"
>
Save
</Button>
)}
</Grid>
</Grid>
</form>
</FormProvider>
</Drawer>
);
};
export default LogForm;

View File

@@ -1,25 +1,25 @@
import { Alert } from '../../interfaces/Alert.interface';
import { LogFormState } from './LogFormState.interface';
import { ILogFormState } from './ILogFormState';
type Action =
| { type: 'SET_ALERT'; payload: Alert | undefined }
| { type: 'SET_IS_DISABLED'; payload: boolean }
| { type: 'SET_IS_LOADING'; payload: boolean }
| { type: 'SET_PILOT_OPTIONS'; payload: { label: string; value: string }[] }
| { type: 'SET_SELECTED_PILOT_NAME'; payload: string };
| { type: 'SET_SELECTED_ENTRY_PILOT_NAME'; payload: string };
export const initialState: LogFormState = {
export const initialState: ILogFormState = {
alert: undefined,
isDisabled: false,
isLoading: true,
pilotOptions: [],
selectedPilotName: ''
selectedEntryPilotName: ''
};
export const reducer = (
state: LogFormState,
state: ILogFormState,
action: Action
): LogFormState => {
): ILogFormState => {
switch (action.type) {
case 'SET_ALERT': {
return {
@@ -45,10 +45,10 @@ export const reducer = (
pilotOptions: action.payload
};
}
case 'SET_SELECTED_PILOT_NAME': {
case 'SET_SELECTED_ENTRY_PILOT_NAME': {
return {
...state,
selectedPilotName: action.payload
selectedEntryPilotName: action.payload
};
}
default: {

View File

@@ -0,0 +1,53 @@
#app {
height: 100%;
}
html,
body {
position: relative;
height: 100%;
}
body {
background: #eee;
font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
font-size: 14px;
color: #000;
margin: 0;
padding: 0;
}
.swiper {
width: 100%;
height: 100%;
}
.swiper-wrapper {
margin-bottom: 20px;
}
.swiper-slide {
text-align: center;
font-size: 18px;
background: #fff;
display: flex;
justify-content: center;
align-items: center;
}
.swiper-slide img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.swiper-pagination-bullet {
background-color: #000000;
height: 13px;
width: 13px;
border: 2px solid #FFFFFF;
}
.swiper-pagination-bullet-active {
box-shadow: 0 0 0 1px #000000;
}

Some files were not shown because too many files have changed in this diff Show More