Compare commits
53 Commits
api-unit-t
...
13f1e86e72
| Author | SHA1 | Date | |
|---|---|---|---|
| 13f1e86e72 | |||
| 25f935171e | |||
| 92fec88ad1 | |||
| 833b1b107c | |||
| 674ae1cf3c | |||
| f0b61703b7 | |||
| c152f06b63 | |||
| 3c9d26e085 | |||
| 12b4f1a65b | |||
| ae03c0200c | |||
| b9668a9d48 | |||
| 19041e3967 | |||
| 5729c72fe1 | |||
| 8760998c78 | |||
| 61f8dc0e1e | |||
| cc7b694fd5 | |||
| 9553c2ec34 | |||
| 0a8c461e3d | |||
| 931d56e753 | |||
| 7ebf78f348 | |||
| 95e2d59abc | |||
| 945081dd7c | |||
| 7213dcf9e1 | |||
| a13ffeaebd | |||
| 620b258e94 | |||
| cc21a43825 | |||
| 112b56080c | |||
| 92f97a93bf | |||
| 4f6e9a072c | |||
| 0c8e4b9fba | |||
| 63d2765d19 | |||
| da76000b8d | |||
| b789e1264a | |||
| 9cc86fe79f | |||
| ee0e10072c | |||
| 899f92f342 | |||
| 5c4245179f | |||
| a7a9f01fd9 | |||
| a367b517db | |||
| fd514e4c8b | |||
| d8513e6dd0 | |||
| 5de5a165da | |||
| 26c46fe6e8 | |||
| 04e5e3e16b | |||
| 0305b424eb | |||
| 8652544471 | |||
| b7b6bdcf4a | |||
| 9c96774f7b | |||
| a9abb5863c | |||
| 477469831f | |||
| 6964531e12 | |||
| 08baaa6d83 | |||
| 487afb08d6 |
@@ -10,12 +10,12 @@ on:
|
|||||||
type: string
|
type: string
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build-and-test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: ${{ inputs.environment_name }}
|
environment: ${{ inputs.environment_name }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: https://gitea.com/actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
sparse-checkout: |
|
sparse-checkout: |
|
||||||
api
|
api
|
||||||
@@ -38,11 +38,15 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
npm run build -w api
|
npm run build -w api
|
||||||
|
|
||||||
|
- name: Test API
|
||||||
|
run: |
|
||||||
|
npm run test -w api
|
||||||
|
|
||||||
- name: Build Client
|
- name: Build Client
|
||||||
env:
|
env:
|
||||||
VITE_API_URL: ${{ vars.VITE_API_URL }}
|
VITE_API_URL: ${{ vars.VITE_API_URL }}
|
||||||
VITE_BASE_URL: ${{ vars.VITE_BASE_URL }}
|
VITE_BASE_URL: ${{ case(inputs.environment_name === 'test', vars.VITE_BASE_URL_TEST, vars.VITE_BASE_URL_PROD) }}
|
||||||
VITE_CLIENT_ID: ${{ vars.VITE_CLIENT_ID }}
|
VITE_CLIENT_ID: ${{ case(inputs.environment_name === 'test', vars.VITE_CLIENT_ID_TEST, vars.VITE_CLIENT_ID_PROD) }}
|
||||||
VITE_ISSUER_URI: ${{ vars.VITE_ISSUER_URI }}
|
VITE_ISSUER_URI: ${{ vars.VITE_ISSUER_URI }}
|
||||||
VITE_TENANT_ID: ${{ vars.VITE_TENANT_ID }}
|
VITE_TENANT_ID: ${{ vars.VITE_TENANT_ID }}
|
||||||
run: |
|
run: |
|
||||||
@@ -7,6 +7,9 @@ on:
|
|||||||
client:
|
client:
|
||||||
value: ${{ jobs.changes.outputs.client }}
|
value: ${{ jobs.changes.outputs.client }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
changes:
|
changes:
|
||||||
name: filter
|
name: filter
|
||||||
@@ -15,7 +18,13 @@ jobs:
|
|||||||
api: ${{ steps.filter.outputs.api }}
|
api: ${{ steps.filter.outputs.api }}
|
||||||
client: ${{ steps.filter.outputs.client }}
|
client: ${{ steps.filter.outputs.client }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- name: 'Setup Node'
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: '22.x'
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: dorny/paths-filter@v3
|
- uses: dorny/paths-filter@v3
|
||||||
id: filter
|
id: filter
|
||||||
@@ -21,11 +21,9 @@ jobs:
|
|||||||
environment: ${{ inputs.environment_name }}
|
environment: ${{ inputs.environment_name }}
|
||||||
steps:
|
steps:
|
||||||
- name: Log in to Azure
|
- name: Log in to Azure
|
||||||
uses: azure/login@v2
|
uses: azure/login@v3
|
||||||
with:
|
with:
|
||||||
client-id: ${{ vars.AZURE_CLIENT_ID }}
|
creds: '{"clientId":"${{ vars.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ vars.AZURE_TENANT_ID }}"}'
|
||||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
|
||||||
tenant-id: ${{ vars.AZURE_TENANT_ID }}
|
|
||||||
|
|
||||||
- name: Azure CLI script
|
- name: Azure CLI script
|
||||||
uses: azure/cli@v2
|
uses: azure/cli@v2
|
||||||
@@ -6,14 +6,14 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
changes:
|
changes:
|
||||||
uses: ./.github/workflows/changes.yaml
|
uses: ./.gitea/workflows/changes.yaml
|
||||||
|
|
||||||
build:
|
build:
|
||||||
if: ${{ needs.changes.outputs.api == 'true' || needs.changes.outputs.client == 'true' }}
|
if: ${{ needs.changes.outputs.api == 'true' || needs.changes.outputs.client == 'true' }}
|
||||||
name: build
|
name: build
|
||||||
needs:
|
needs:
|
||||||
- changes
|
- changes
|
||||||
uses: ./.github/workflows/build.yaml
|
uses: ./.gitea/workflows/build_and_test.yaml
|
||||||
with:
|
with:
|
||||||
environment_name: test
|
environment_name: test
|
||||||
version_number: ${{ github.run_id }}
|
version_number: ${{ github.run_id }}
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- changes
|
- changes
|
||||||
- build
|
- build
|
||||||
uses: ./.github/workflows/deploy.yaml
|
uses: ./.gitea/workflows/deploy.yaml
|
||||||
with:
|
with:
|
||||||
app_name: flying
|
app_name: flying
|
||||||
environment_name: test
|
environment_name: test
|
||||||
@@ -5,14 +5,14 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
changes:
|
changes:
|
||||||
uses: ./.github/workflows/changes.yaml
|
uses: ./.gitea/workflows/changes.yaml
|
||||||
|
|
||||||
build:
|
build:
|
||||||
if: ${{ needs.changes.outputs.app == 'true' || needs.changes.outputs.client == 'true'}}
|
if: ${{ needs.changes.outputs.api == 'true' || needs.changes.outputs.client == 'true'}}
|
||||||
name: build
|
name: build-and-test
|
||||||
needs:
|
needs:
|
||||||
- changes
|
- changes
|
||||||
uses: ./.github/workflows/build.yaml
|
uses: ./.gitea/workflows/build_and_test.yaml
|
||||||
with:
|
with:
|
||||||
environment_name: pull_request
|
environment_name: pull_request
|
||||||
version_number: ${{ github.run_id }}
|
version_number: ${{ github.run_id }}
|
||||||
@@ -7,7 +7,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: build
|
name: build
|
||||||
uses: ./.github/workflows/build.yaml
|
uses: ./.gitea/workflows/build_and_test.yaml
|
||||||
with:
|
with:
|
||||||
environment_name: prod
|
environment_name: prod
|
||||||
version_number: ${{ github.ref_name }}
|
version_number: ${{ github.ref_name }}
|
||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
name: deploy
|
name: deploy
|
||||||
needs:
|
needs:
|
||||||
- build
|
- build
|
||||||
uses: ./.github/workflows/deploy.yaml
|
uses: ./.gitea/workflows/deploy.yaml
|
||||||
with:
|
with:
|
||||||
app_name: flying
|
app_name: flying
|
||||||
environment_name: prod
|
environment_name: prod
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "api",
|
"name": "api",
|
||||||
"version": "2.0.0-beta-4",
|
"version": "2.1.4",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PilotEntity } from 'src/pilot/pilot.entity';
|
import { PilotEntity } from '../pilot/pilot.entity';
|
||||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
|
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
|
||||||
@Entity({ name: 'endorsements' })
|
@Entity({ name: 'endorsements' })
|
||||||
|
|||||||
@@ -48,10 +48,10 @@ import { ConfigService } from '@nestjs/config';
|
|||||||
return fileUrl;
|
return fileUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadFile(containerName: string, rowKey: string, fileName: string): Promise<string> {
|
async downloadFile(containerName: string, logId: string, fileName: string): Promise<string> {
|
||||||
this.containerName = containerName;
|
this.containerName = containerName;
|
||||||
|
|
||||||
const blockBlobClient = await this.getBlobClient(`${rowKey}/${fileName}`);
|
const blockBlobClient = await this.getBlobClient(`${logId}/${fileName}`);
|
||||||
const downloadBlockBlobResponse = await blockBlobClient.download();
|
const downloadBlockBlobResponse = await blockBlobClient.download();
|
||||||
const downloaded: string = (await this.streamToBuffer(downloadBlockBlobResponse.readableStreamBody)).toString()
|
const downloaded: string = (await this.streamToBuffer(downloadBlockBlobResponse.readableStreamBody)).toString()
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +1,25 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
import { HealthController } from './health.controller';
|
import { HealthController } from "./health.controller"
|
||||||
import { HealthService } from './health.service';
|
import { HttpStatus } from "@nestjs/common";
|
||||||
|
|
||||||
describe('HealthController', () => {
|
describe('HealthController', () => {
|
||||||
let controller; HealthController;
|
let controller: HealthController;
|
||||||
|
|
||||||
const mockHealthService = {
|
|
||||||
isDatabaseConnected: jest.fn()
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
controllers: [HealthController],
|
controllers: [HealthController]
|
||||||
providers: [HealthService]
|
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
controller = module.get<HealthController>(HealthController);
|
controller = module.get<HealthController>(HealthController);
|
||||||
})
|
})
|
||||||
|
|
||||||
it('isHealthy => should return true', () => {
|
it('should be defined', () => {
|
||||||
expect(controller).toBeDefined();
|
expect(controller).toBeDefined();
|
||||||
});
|
|
||||||
|
|
||||||
it('should return database connected', async () => {
|
|
||||||
jest.spyOn(mockHealthService, 'isDatabaseConnected').mockReturnValue(true);
|
|
||||||
|
|
||||||
const result = await controller.isHealthy();
|
|
||||||
|
|
||||||
expect(mockHealthService.isDatabaseConnected).toHaveBeenCalled();
|
|
||||||
expect(result).toEqual(true);
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('isHealthy => should return error', async () => {
|
it('isHealth => should return status ok', async () => {
|
||||||
jest.spyOn(mockHealthService, 'isDatabaseConnected').mockReturnValue(false);
|
|
||||||
|
|
||||||
const result = await controller.isHealthy();
|
const result = await controller.isHealthy();
|
||||||
|
|
||||||
expect(mockHealthService.isDatabaseConnected).toHaveBeenCalled();
|
expect(result).toEqual(HttpStatus.OK);
|
||||||
expect(result).toEqual(false);
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -1,27 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
HttpException,
|
HttpStatus,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { HealthService } from './health.service';
|
|
||||||
import { CustomError } from 'src/error/customError';
|
|
||||||
|
|
||||||
|
|
||||||
@Controller('health')
|
@Controller('health')
|
||||||
export class HealthController {
|
export class HealthController {
|
||||||
constructor(
|
|
||||||
private readonly healthService: HealthService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
async isHealthy(): Promise<boolean> {
|
async isHealthy(): Promise<HttpStatus> {
|
||||||
try {
|
return HttpStatus.OK
|
||||||
return await this.healthService.isDatabaseConnected();
|
|
||||||
} catch (error) {
|
|
||||||
const customError = error as CustomError;
|
|
||||||
|
|
||||||
throw new HttpException(customError.message, customError.statusCode);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { HealthController } from './health.controller';
|
import { HealthController } from './health.controller';
|
||||||
import { HealthService } from './health.service';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [HealthController],
|
controllers: [HealthController],
|
||||||
providers: [
|
|
||||||
HealthService
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
export class HealthModule {}
|
export class HealthModule {}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { HealthService } from './health.service';
|
|
||||||
|
|
||||||
describe('HealthService', () => {
|
|
||||||
let service: HealthService;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [HealthService]
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
service = module.get<HealthService>(HealthService);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { CustomError } from '../error/customError';
|
|
||||||
import { DataSource } from 'typeorm';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class HealthService {
|
|
||||||
constructor(private dataSource: DataSource) {}
|
|
||||||
|
|
||||||
async isDatabaseConnected(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const isDatabaseConnected: boolean = this.dataSource.isInitialized;
|
|
||||||
|
|
||||||
if (isDatabaseConnected) {
|
|
||||||
return isDatabaseConnected;
|
|
||||||
} else {
|
|
||||||
throw new CustomError('Database not connected', 'Database not connected', 400)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { JwtPayload } from "jwt-decode";
|
|
||||||
|
|
||||||
export interface CustomJwtPayload extends JwtPayload {
|
|
||||||
permissions: string[];
|
|
||||||
}
|
|
||||||
449
api/src/log/log.controller.spec.ts
Normal file
449
api/src/log/log.controller.spec.ts
Normal file
@@ -0,0 +1,449 @@
|
|||||||
|
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(),
|
||||||
|
findLogsWithCount: jest.fn(),
|
||||||
|
findLogsWithTracks: 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 log = {
|
||||||
|
id: '95834f84-0a02-44d3-884e-a20237adeca0',
|
||||||
|
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(mockLogService, 'find').mockReturnValue(log);
|
||||||
|
|
||||||
|
const result = await controller.find(log.id);
|
||||||
|
|
||||||
|
expect(result).toEqual(log);
|
||||||
|
expect(mockLogService.find).toHaveBeenCalled();
|
||||||
|
expect(mockLogService.find).toHaveBeenCalledWith(log.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('findLogsWithCount => should find logs with count', async () => {
|
||||||
|
const log = {
|
||||||
|
id: '95834f84-0a02-44d3-884e-a20237adeca0',
|
||||||
|
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: [
|
||||||
|
{
|
||||||
|
"id": "4645ce0c-6fd7-432c-8089-777a7139cf7e",
|
||||||
|
"url": "http://127.0.0.1:10000/devstoreaccount1/tracks/0e7a641a-0264-47f0-88bf-7c296c842d8b/FlightAware_N70392_KANE_KONA_20250628.kml",
|
||||||
|
"order": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5ce26898-a18b-46f4-b989-3fb7acdace08",
|
||||||
|
"url": "http://127.0.0.1:10000/devstoreaccount1/tracks/0e7a641a-0264-47f0-88bf-7c296c842d8b/FlightAware_N70392_KONA_KANE_20250628.kml",
|
||||||
|
"order": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} as LogEntity;
|
||||||
|
const logs = [log]
|
||||||
|
const count = 1
|
||||||
|
const morePages = false
|
||||||
|
|
||||||
|
jest.spyOn(mockLogService, 'findLogsWithCount').mockReturnValue({
|
||||||
|
entities: logs,
|
||||||
|
total: count,
|
||||||
|
hasNextPage: morePages
|
||||||
|
});
|
||||||
|
|
||||||
|
const {entities, total, hasNextPage} = await controller.findLogsWithCount();
|
||||||
|
|
||||||
|
expect(entities).toEqual(logs);
|
||||||
|
expect(total).toEqual(count);
|
||||||
|
expect(hasNextPage).toEqual(morePages)
|
||||||
|
expect(mockLogService.findLogsWithCount).toHaveBeenCalled();
|
||||||
|
})
|
||||||
|
|
||||||
|
it('findLogsWithCount => should fail to find logs with count', async () => {
|
||||||
|
jest.spyOn(mockLogService, 'findLogsWithCount').mockRejectedValue(new Error('Logs not found'))
|
||||||
|
|
||||||
|
try {
|
||||||
|
await controller.findLogsWithCount();
|
||||||
|
|
||||||
|
fail('findLogsWithCount did not throw error');
|
||||||
|
} catch (error) {
|
||||||
|
expect(error).toBeInstanceOf(Error);
|
||||||
|
expect(mockLogService.findLogsWithCount).toHaveBeenCalled();
|
||||||
|
expect(mockLogService.findLogsWithCount).rejects.toThrow('Logs not found')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('findLogsWithTracks => should find logs with tracks', async () => {
|
||||||
|
const log = {
|
||||||
|
id: '95834f84-0a02-44d3-884e-a20237adeca0',
|
||||||
|
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: [
|
||||||
|
{
|
||||||
|
"id": "4645ce0c-6fd7-432c-8089-777a7139cf7e",
|
||||||
|
"url": "http://127.0.0.1:10000/devstoreaccount1/tracks/0e7a641a-0264-47f0-88bf-7c296c842d8b/FlightAware_N70392_KANE_KONA_20250628.kml",
|
||||||
|
"order": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5ce26898-a18b-46f4-b989-3fb7acdace08",
|
||||||
|
"url": "http://127.0.0.1:10000/devstoreaccount1/tracks/0e7a641a-0264-47f0-88bf-7c296c842d8b/FlightAware_N70392_KONA_KANE_20250628.kml",
|
||||||
|
"order": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} as LogEntity;
|
||||||
|
const logs = [log]
|
||||||
|
const count = 1
|
||||||
|
const morePages = false
|
||||||
|
|
||||||
|
jest.spyOn(mockLogService, 'findLogsWithTracks').mockReturnValue({
|
||||||
|
entities: logs,
|
||||||
|
total: count,
|
||||||
|
hasNextPage: morePages
|
||||||
|
});
|
||||||
|
|
||||||
|
const {entities, total, hasNextPage} = await controller.findLogsWithTracks();
|
||||||
|
|
||||||
|
expect(entities).toEqual(logs);
|
||||||
|
expect(total).toEqual(count);
|
||||||
|
expect(hasNextPage).toEqual(morePages)
|
||||||
|
expect(mockLogService.findLogsWithTracks).toHaveBeenCalled();
|
||||||
|
})
|
||||||
|
|
||||||
|
it('findLogsWithCount => should fail to find logs with tracks', async () => {
|
||||||
|
jest.spyOn(mockLogService, 'findLogsWithTracks').mockRejectedValue(new Error('Logs not found'))
|
||||||
|
|
||||||
|
try {
|
||||||
|
await controller.findLogsWithTracks();
|
||||||
|
|
||||||
|
fail('findLogsWithTracks did not throw error');
|
||||||
|
} catch (error) {
|
||||||
|
expect(error).toBeInstanceOf(Error);
|
||||||
|
expect(mockLogService.findLogsWithTracks).toHaveBeenCalled();
|
||||||
|
expect(mockLogService.findLogsWithTracks).rejects.toThrow('Logs not found')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('create => should create a new log', 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;
|
||||||
|
|
||||||
|
const log = {
|
||||||
|
id: '95834f84-0a02-44d3-884e-a20237adeca0',
|
||||||
|
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(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 fail to create a new log', 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(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 = {
|
||||||
|
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(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 = {
|
||||||
|
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(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')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Param,
|
Param,
|
||||||
Post,
|
Post,
|
||||||
Put,
|
Put,
|
||||||
|
Query,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
UseInterceptors
|
UseInterceptors
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
@@ -19,6 +20,7 @@ import { LogInterceptor } from './log.interceptor';
|
|||||||
import { FileService } from '../file/file.service';
|
import { FileService } from '../file/file.service';
|
||||||
import { DeleteResult, InsertResult, UpdateResult } from 'typeorm';
|
import { DeleteResult, InsertResult, UpdateResult } from 'typeorm';
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { Logs } from './logs.interface';
|
||||||
|
|
||||||
const reflector = new Reflector();
|
const reflector = new Reflector();
|
||||||
|
|
||||||
@@ -30,7 +32,30 @@ export class LogController {
|
|||||||
private readonly logService: LogService
|
private readonly logService: LogService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@Public()
|
||||||
|
async findLogsWithCount(@Query('skip') skip?, @Query('take') take?: number,): Promise<Logs> {
|
||||||
|
try {
|
||||||
|
return await this.logService.findLogsWithCount(skip, take)
|
||||||
|
} catch (error) {
|
||||||
|
const customError = error as CustomError;
|
||||||
|
|
||||||
|
throw new HttpException(customError.message, customError.statusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('flights')
|
||||||
|
@Public()
|
||||||
|
async findLogsWithTracks(@Query('skip') skip?, @Query('take') take?: number,): Promise<Logs> {
|
||||||
|
try {
|
||||||
|
return await this.logService.findLogsWithTracks(skip, take)
|
||||||
|
} catch (error) {
|
||||||
|
const customError = error as CustomError;
|
||||||
|
|
||||||
|
throw new HttpException(customError.message, customError.statusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@Public()
|
@Public()
|
||||||
async find(
|
async find(
|
||||||
@@ -44,28 +69,15 @@ export class LogController {
|
|||||||
throw new HttpException(customError.message, customError.statusCode);
|
throw new HttpException(customError.message, customError.statusCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
|
||||||
@Public()
|
|
||||||
async findAll(): Promise<LogEntity[]> {
|
|
||||||
try {
|
|
||||||
return await this.logService.findAll();
|
|
||||||
} catch (error) {
|
|
||||||
const customError = error as CustomError;
|
|
||||||
|
|
||||||
throw new HttpException(customError.message, customError.statusCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
async create(@Body() logDto: LogDto): Promise<InsertResult> {
|
async create(@Body() logDto: LogDto) {
|
||||||
try {
|
try {
|
||||||
return await this.logService.create(logDto);
|
return await this.logService.create(logDto);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const customError = error as CustomError;
|
const customError = error as CustomError;
|
||||||
console.log(error)
|
|
||||||
throw new HttpException(customError.message, customError.statusCode);
|
throw new HttpException(customError.message, customError.statusCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,13 +87,12 @@ export class LogController {
|
|||||||
async update(
|
async update(
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@Body() logDto: LogDto
|
@Body() logDto: LogDto
|
||||||
): Promise<UpdateResult> {
|
) {
|
||||||
try {
|
try {
|
||||||
console.log(logDto);
|
|
||||||
return await this.logService.update(id, logDto);
|
return await this.logService.update(id, logDto);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const customError = error as CustomError;
|
const customError = error as CustomError;
|
||||||
console.log(customError)
|
|
||||||
throw new HttpException(customError.message, customError.statusCode);
|
throw new HttpException(customError.message, customError.statusCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { PilotEntity } from "src/pilot/pilot.entity";
|
|
||||||
|
|
||||||
export class LogDto {
|
export class LogDto {
|
||||||
pilotId: string;
|
pilotId: string;
|
||||||
date: Date;
|
date: Date;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { PilotEntity } from 'src/pilot/pilot.entity';
|
import { PilotEntity } from '../pilot/pilot.entity';
|
||||||
import { TrackEntity } from 'src/track/track.entity';
|
import { TrackEntity } from '../track/track.entity';
|
||||||
import { ColumnNumericTransformer } from 'src/transformers/columnNumeric.transformer';
|
import { ColumnNumericTransformer } from '../transformers/columnNumeric.transformer';
|
||||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, OneToMany } from 'typeorm';
|
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, OneToMany } from 'typeorm';
|
||||||
|
|
||||||
@Entity({ name: 'logs' })
|
@Entity({ name: 'logs' })
|
||||||
|
|||||||
@@ -15,23 +15,21 @@ export class LogInterceptor implements NestInterceptor {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return handler.handle().pipe(
|
return handler.handle().pipe(
|
||||||
map((data: LogEntity[]) => {
|
map((data: any) => {
|
||||||
const req = context.switchToHttp().getRequest();
|
const req = context.switchToHttp().getRequest();
|
||||||
const limitData = (data) => {
|
const limitData = (log: LogEntity) => {
|
||||||
return data.map((log: LogEntity) => {
|
return {
|
||||||
return {
|
id: log.id,
|
||||||
id: log.id,
|
pilot: {
|
||||||
pilot: {
|
name: log.pilot.name
|
||||||
name: log.pilot.name
|
},
|
||||||
},
|
date: log.date,
|
||||||
date: log.date,
|
aircraftMakeModel: log.aircraftMakeModel,
|
||||||
aircraftMakeModel: log.aircraftMakeModel,
|
routeFrom: log.routeFrom,
|
||||||
routeFrom: log.routeFrom,
|
routeTo: log.routeTo,
|
||||||
routeTo: log.routeTo,
|
durationOfFlight: log.durationOfFlight,
|
||||||
durationOfFlight: log.durationOfFlight,
|
tracks: log.tracks,
|
||||||
tracks: log.tracks,
|
};
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.headers.authorization) {
|
if (req.headers.authorization) {
|
||||||
@@ -41,17 +39,38 @@ export class LogInterceptor implements NestInterceptor {
|
|||||||
const rolesKeyName = Object.keys(jwtPayload).find((key) => key.includes('roles'));
|
const rolesKeyName = Object.keys(jwtPayload).find((key) => key.includes('roles'));
|
||||||
|
|
||||||
if (jwtPayload[rolesKeyName].includes('Flying.Read')) {
|
if (jwtPayload[rolesKeyName].includes('Flying.Read')) {
|
||||||
const logs = limitData(data);
|
if (data.entities) {
|
||||||
|
const logs = data.entities.map((entity) => limitData(entity));
|
||||||
|
|
||||||
return logs;
|
return {
|
||||||
|
entities: logs,
|
||||||
|
total: data.total,
|
||||||
|
hasNextPage: data.hasNextPage
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const log = limitData(data);
|
||||||
|
|
||||||
|
return log;
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
} else if (!req.headers.authorization && isPublic) {
|
} else if (!req.headers.authorization && isPublic) {
|
||||||
const publicData = limitData(data)
|
if (data.entities) {
|
||||||
const logs = publicData.slice(0, 5)
|
const publicData = data.entities.map((entity) => limitData(entity))
|
||||||
|
const logs = publicData.slice(0, 5)
|
||||||
|
|
||||||
return logs;
|
return {
|
||||||
|
entities: logs,
|
||||||
|
total: logs.length,
|
||||||
|
hasNextPage: false
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const publicData = limitData(data);
|
||||||
|
|
||||||
|
return publicData
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { LogEntity } from './log.entity';
|
|||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { FileService } from '../file/file.service';
|
import { FileService } from '../file/file.service';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { PilotModule } from 'src/pilot/pilot.module';
|
import { PilotModule } from '../pilot/pilot.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|||||||
366
api/src/log/log.service.spec.ts
Normal file
366
api/src/log/log.service.spec.ts
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
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 mockQueryBuilder = {
|
||||||
|
createQueryBuilder: jest.fn().mockReturnThis(),
|
||||||
|
innerJoinAndSelect: jest.fn().mockReturnThis(),
|
||||||
|
orderBy: jest.fn().mockReturnThis(),
|
||||||
|
skip: jest.fn().mockReturnThis(),
|
||||||
|
take: jest.fn().mockReturnThis(),
|
||||||
|
getManyAndCount: jest.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockLogRepository = {
|
||||||
|
createQueryBuilder: jest.fn().mockReturnValue(mockQueryBuilder),
|
||||||
|
delete: jest.fn(),
|
||||||
|
findAndCount: 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 => should 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('findLogsWithCount => should find log entries with count', 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: [
|
||||||
|
{
|
||||||
|
"id": "4645ce0c-6fd7-432c-8089-777a7139cf7e",
|
||||||
|
"url": "http://127.0.0.1:10000/devstoreaccount1/tracks/0e7a641a-0264-47f0-88bf-7c296c842d8b/FlightAware_N70392_KANE_KONA_20250628.kml",
|
||||||
|
"order": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5ce26898-a18b-46f4-b989-3fb7acdace08",
|
||||||
|
"url": "http://127.0.0.1:10000/devstoreaccount1/tracks/0e7a641a-0264-47f0-88bf-7c296c842d8b/FlightAware_N70392_KONA_KANE_20250628.kml",
|
||||||
|
"order": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} as LogEntity;
|
||||||
|
const logs = [log];
|
||||||
|
const count = 1
|
||||||
|
const morePages = false
|
||||||
|
|
||||||
|
jest.spyOn(mockQueryBuilder, 'getManyAndCount').mockReturnValue([logs, count, morePages]);
|
||||||
|
|
||||||
|
const {entities, total, hasNextPage} = await service.findLogsWithCount();
|
||||||
|
|
||||||
|
expect(entities).toEqual(logs);
|
||||||
|
expect(count).toEqual(total);
|
||||||
|
expect(hasNextPage).toEqual(morePages);
|
||||||
|
expect(mockQueryBuilder.getManyAndCount).toHaveBeenCalled();
|
||||||
|
})
|
||||||
|
|
||||||
|
it('findLogsWithTracks => should find log entries with tracks', 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: [
|
||||||
|
{
|
||||||
|
"id": "4645ce0c-6fd7-432c-8089-777a7139cf7e",
|
||||||
|
"url": "http://127.0.0.1:10000/devstoreaccount1/tracks/0e7a641a-0264-47f0-88bf-7c296c842d8b/FlightAware_N70392_KANE_KONA_20250628.kml",
|
||||||
|
"order": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5ce26898-a18b-46f4-b989-3fb7acdace08",
|
||||||
|
"url": "http://127.0.0.1:10000/devstoreaccount1/tracks/0e7a641a-0264-47f0-88bf-7c296c842d8b/FlightAware_N70392_KONA_KANE_20250628.kml",
|
||||||
|
"order": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} as LogEntity;
|
||||||
|
const logs = [log];
|
||||||
|
const count = 1
|
||||||
|
const morePages = false
|
||||||
|
|
||||||
|
jest.spyOn(mockQueryBuilder, 'getManyAndCount').mockResolvedValue([logs, count, morePages])
|
||||||
|
|
||||||
|
const {entities, total, hasNextPage} = await service.findLogsWithTracks();
|
||||||
|
|
||||||
|
expect(entities).toEqual(logs);
|
||||||
|
expect(count).toEqual(total);
|
||||||
|
expect(hasNextPage).toEqual(morePages);
|
||||||
|
expect(mockQueryBuilder.getManyAndCount).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);
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -3,10 +3,11 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { LogEntity } from './log.entity';
|
import { LogEntity } from './log.entity';
|
||||||
import { DeleteResult, InsertResult, Repository, UpdateResult } from 'typeorm';
|
import { DeleteResult, InsertResult, Repository, UpdateResult } from 'typeorm';
|
||||||
import { LogDto } from './log.dto';
|
import { LogDto } from './log.dto';
|
||||||
import { PilotService } from 'src/pilot/pilot.service';
|
import { PilotService } from '../pilot/pilot.service';
|
||||||
import { PilotEntity } from 'src/pilot/pilot.entity';
|
import { PilotEntity } from '../pilot/pilot.entity';
|
||||||
import { CustomError } from 'src/error/customError';
|
import { CustomError } from '../error/customError';
|
||||||
import { FileService } from 'src/file/file.service';
|
import { FileService } from '../file/file.service';
|
||||||
|
import { Logs } from './logs.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LogService {
|
export class LogService {
|
||||||
@@ -17,46 +18,65 @@ export class LogService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async find(id: string): Promise<LogEntity> {
|
async find(id: string): Promise<LogEntity> {
|
||||||
const logEntity: LogEntity = await this.logRepository.findOne({
|
return await this.logRepository.findOne({
|
||||||
where: { id: id },
|
where: { id: id },
|
||||||
relations: ['pilot', 'tracks']
|
relations: ['pilot', 'tracks']
|
||||||
});
|
});
|
||||||
|
|
||||||
return logEntity;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAll(): Promise<LogEntity[]> {
|
async findLogsWithCount(skip?: number, take?: number): Promise<Logs> {
|
||||||
return await this.logRepository.find({
|
// const [entities, total] = await this.logRepository.findAndCount({
|
||||||
relations: ['pilot', 'tracks']
|
// take,
|
||||||
});
|
// skip,
|
||||||
}
|
// order: {
|
||||||
|
// date: 'DESC'
|
||||||
|
// },
|
||||||
|
// relations: ['pilot', 'tracks']
|
||||||
|
// })
|
||||||
|
|
||||||
async create(logDto: LogDto): Promise<InsertResult> {
|
const [entities, total] = await this.logRepository
|
||||||
try{
|
.createQueryBuilder('logs')
|
||||||
const pilotEntity: PilotEntity = await this.pilotService.find(logDto.pilotId);
|
.innerJoinAndSelect('logs.pilot', 'pilot')
|
||||||
|
.orderBy('logs.date', 'DESC')
|
||||||
|
.skip(skip)
|
||||||
|
.take(take)
|
||||||
|
.getManyAndCount()
|
||||||
|
|
||||||
if (pilotEntity) {
|
return {
|
||||||
const { pilotId, ...newLogDto } = logDto;
|
entities,
|
||||||
const log = this.logRepository.create({
|
total,
|
||||||
...newLogDto,
|
hasNextPage: skip + take < total
|
||||||
pilot: pilotEntity
|
|
||||||
})
|
|
||||||
|
|
||||||
return this.logRepository.insert(log);
|
|
||||||
} else {
|
|
||||||
throw new CustomError('Pilot not found', 'Not found', 404);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(id: string, logDto: LogDto): Promise<UpdateResult> {
|
async findLogsWithTracks(skip?: number, take?: number): Promise<Logs> {
|
||||||
const log = logDto
|
const [entities, total] = await this.logRepository
|
||||||
|
.createQueryBuilder('logs')
|
||||||
|
.innerJoinAndSelect('logs.tracks', 'track')
|
||||||
|
.innerJoinAndSelect('logs.pilot', 'pilot')
|
||||||
|
.orderBy('logs.date', 'DESC')
|
||||||
|
.skip(skip)
|
||||||
|
.take(take)
|
||||||
|
.getManyAndCount();
|
||||||
|
|
||||||
delete log.tracks;
|
return {
|
||||||
|
entities,
|
||||||
|
total,
|
||||||
|
hasNextPage: skip + take < total
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return await this.logRepository.update(id, log);
|
async create(logDto: LogDto): Promise<LogDto> {
|
||||||
|
return this.logRepository.save(logDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 delete(id: string): Promise<DeleteResult> {
|
async delete(id: string): Promise<DeleteResult> {
|
||||||
|
|||||||
7
api/src/log/logs.interface.ts
Normal file
7
api/src/log/logs.interface.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { LogEntity } from "src/log/log.entity";
|
||||||
|
|
||||||
|
export interface Logs {
|
||||||
|
entities: LogEntity[],
|
||||||
|
total: number,
|
||||||
|
hasNextPage: boolean
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PilotEntity } from 'src/pilot/pilot.entity';
|
import { PilotEntity } from '../pilot/pilot.entity';
|
||||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
|
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
|
||||||
@Entity({ name: 'medical' })
|
@Entity({ name: 'medical' })
|
||||||
|
|||||||
260
api/src/pilot/pilot.controller.spec.ts
Normal file
260
api/src/pilot/pilot.controller.spec.ts
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
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')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -50,9 +50,8 @@ export class PilotController {
|
|||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
async create(@Body() pilotDto: PilotDto) {
|
async create(@Body() pilotDto: PilotDto): Promise<PilotDto> {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
return await this.pilotService.create(pilotDto);
|
return await this.pilotService.create(pilotDto);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const customError = error as CustomError;
|
const customError = error as CustomError;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { LogEntity } from "src/log/log.entity";
|
|
||||||
|
|
||||||
export class PilotDto {
|
export class PilotDto {
|
||||||
name: string;
|
name: string;
|
||||||
address: string;
|
address: string;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { LogEntity } from 'src/log/log.entity';
|
import { LogEntity } from '../log/log.entity';
|
||||||
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
|
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
|
||||||
import { CertificateEntity } from '../certificate/certificate.entity';
|
import { CertificateEntity } from '../certificate/certificate.entity';
|
||||||
import { EndorsementEntity } from 'src/endorsement/endorsement.entity';
|
import { EndorsementEntity } from '../endorsement/endorsement.entity';
|
||||||
import { MedicalEntity } from 'src/medical/medical.entity';
|
import { MedicalEntity } from '../medical/medical.entity';
|
||||||
|
|
||||||
@Entity({ name: 'pilots' })
|
@Entity({ name: 'pilots' })
|
||||||
export class PilotEntity {
|
export class PilotEntity {
|
||||||
|
|||||||
206
api/src/pilot/pilot.service.spec.ts
Normal file
206
api/src/pilot/pilot.service.spec.ts
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
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 });
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -3,7 +3,7 @@ import { PilotEntity } from './pilot.entity';
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { DeleteResult, InsertResult, Repository, UpdateResult } from 'typeorm';
|
import { DeleteResult, InsertResult, Repository, UpdateResult } from 'typeorm';
|
||||||
import { PilotDto } from './pilot.dto';
|
import { PilotDto } from './pilot.dto';
|
||||||
import { CustomError } from 'src/error/customError';
|
import { CustomError } from '../error/customError';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PilotService {
|
export class PilotService {
|
||||||
@@ -13,7 +13,7 @@ export class PilotService {
|
|||||||
|
|
||||||
async find(id: string): Promise<PilotEntity> {
|
async find(id: string): Promise<PilotEntity> {
|
||||||
try {
|
try {
|
||||||
const pilotEntity = await this.pilotRepository.findOneBy({ id });
|
const pilotEntity: PilotEntity = await this.pilotRepository.findOneBy({ id });
|
||||||
|
|
||||||
if (pilotEntity) {
|
if (pilotEntity) {
|
||||||
return pilotEntity
|
return pilotEntity
|
||||||
@@ -29,15 +29,18 @@ export class PilotService {
|
|||||||
return await this.pilotRepository.find();
|
return await this.pilotRepository.find();
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(pilot: PilotDto): Promise<InsertResult> {
|
async create(pilot: PilotDto): Promise<PilotDto> {
|
||||||
return await this.pilotRepository.insert(pilot);
|
return await this.pilotRepository.save(pilot);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(
|
async update(
|
||||||
id: string,
|
id: string,
|
||||||
pilot: PilotDto
|
pilotDto: PilotDto
|
||||||
): Promise<UpdateResult> {
|
): Promise<PilotDto> {
|
||||||
return await this.pilotRepository.update(id, pilot);
|
const pilotEntity: PilotEntity = await this.pilotRepository.findOneBy({ id })
|
||||||
|
const pilotEntityUpdated = Object.assign(pilotEntity, pilotDto)
|
||||||
|
|
||||||
|
return await this.pilotRepository.save(pilotEntityUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: string): Promise<DeleteResult> {
|
async delete(id: string): Promise<DeleteResult> {
|
||||||
|
|||||||
218
api/src/track/track.controller.spec.ts
Normal file
218
api/src/track/track.controller.spec.ts
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
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')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LogEntity } from 'src/log/log.entity';
|
import { LogEntity } from '../log/log.entity';
|
||||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
|
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
|
||||||
@Entity({ name: 'tracks' })
|
@Entity({ name: 'tracks' })
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { TrackEntity } from "./track.entity";
|
|||||||
import { TrackController } from "./track.controller";
|
import { TrackController } from "./track.controller";
|
||||||
import { FileService } from "../file/file.service";
|
import { FileService } from "../file/file.service";
|
||||||
import { TrackService } from './track.service';
|
import { TrackService } from './track.service';
|
||||||
import { LogModule } from 'src/log/log.module';
|
import { LogModule } from '../log/log.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|||||||
346
api/src/track/track.service.spec.ts
Normal file
346
api/src/track/track.service.spec.ts
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -3,10 +3,11 @@ import { TrackDto } from "./track.dto";
|
|||||||
import { TrackEntity } from "./track.entity";
|
import { TrackEntity } from "./track.entity";
|
||||||
import { InjectRepository } from "@nestjs/typeorm";
|
import { InjectRepository } from "@nestjs/typeorm";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
import { LogService } from "src/log/log.service";
|
import { LogService } from "../log/log.service";
|
||||||
import { LogEntity } from "src/log/log.entity";
|
import { LogEntity } from "../log/log.entity";
|
||||||
import { CustomError } from "src/error/customError";
|
import { CustomError } from "../error/customError";
|
||||||
import { FileService } from "src/file/file.service";
|
import { FileService } from "../file/file.service";
|
||||||
|
import { Logs } from "src/log/logs.interface";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TrackService {
|
export class TrackService {
|
||||||
@@ -19,11 +20,7 @@ export class TrackService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async find(id: string): Promise<TrackEntity> {
|
async find(id: string): Promise<TrackEntity> {
|
||||||
try {
|
return await this.trackRepository.findOneBy({ id });
|
||||||
return await this.trackRepository.findOneBy({ id });
|
|
||||||
} catch (error) {
|
|
||||||
throw new CustomError('Track not found', 'Not found', 404)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAll(logId: string): Promise<TrackEntity[]> {
|
async findAll(logId: string): Promise<TrackEntity[]> {
|
||||||
@@ -40,10 +37,11 @@ export class TrackService {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return tracks;
|
return tracks;
|
||||||
|
} else {
|
||||||
|
throw new CustomError('Tracks not found', 'Not found', 404);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
throw error
|
||||||
throw new CustomError('Tracks not found', 'Not found', 404);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,21 +67,11 @@ export class TrackService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async update(id: string, track: TrackDto): Promise<UpdateResult> {
|
async update(id: string, track: TrackDto): Promise<UpdateResult> {
|
||||||
try {
|
return await this.trackRepository.update(id, track);
|
||||||
return await this.trackRepository.update(id, track);
|
|
||||||
} catch(error) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadTrackFile(logId: string, fileName: string): Promise<string> {
|
async downloadTrackFile(logId: string, fileName: string): Promise<string> {
|
||||||
try {
|
return await this.fileService.downloadFile(this.containerName, logId, fileName);
|
||||||
const downloadedFile: string = await this.fileService.downloadFile(this.containerName, logId, fileName);
|
|
||||||
|
|
||||||
return downloadedFile;
|
|
||||||
} catch (error) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: string, logId: string, fileName: string): Promise<DeleteResult> {
|
async delete(id: string, logId: string, fileName: string): Promise<DeleteResult> {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "client",
|
"name": "client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.0.0-beta-4",
|
"version": "2.1.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -3,34 +3,70 @@ import { useEffect, useReducer } from "react";
|
|||||||
import { useLogs } from "../../hooks/logs/UseLogs";
|
import { useLogs } from "../../hooks/logs/UseLogs";
|
||||||
import { LogbookEntry } from "../logbook/LogbookEntry.interface";
|
import { LogbookEntry } from "../logbook/LogbookEntry.interface";
|
||||||
import { initialState, reducer } from "./reducer";
|
import { initialState, reducer } from "./reducer";
|
||||||
|
import { useOidc } from "../../auth/oidcConfig";
|
||||||
import Alert from "../alert/Alert";
|
import Alert from "../alert/Alert";
|
||||||
|
import { AxiosError, AxiosResponse } from "axios";
|
||||||
|
import httpClient from "../../httpClient/httpClient";
|
||||||
|
import { useBreakpoints } from "../../hooks/useBreakpoints/UseBreakpoints";
|
||||||
|
import { ScreenSize } from "../../enums/screenSize";
|
||||||
|
|
||||||
const Flights = () => {
|
const Flights = () => {
|
||||||
const [state, dispatch] = useReducer(reducer, initialState);
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
const { logs, logsLoading } = useLogs();
|
const { isUserLoggedIn } = useOidc();
|
||||||
|
const { screenSize } = useBreakpoints();
|
||||||
|
|
||||||
|
const getFlights = async (pageIndex: number, pageSize: number) => {
|
||||||
|
try {
|
||||||
|
const response: AxiosResponse = await httpClient.get(`api/logs/flights`, {
|
||||||
|
params: {
|
||||||
|
skip: pageIndex,
|
||||||
|
take: pageSize
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const flights: LogbookEntry[] = response.data.entities;
|
||||||
|
const hasMore: boolean = response.data.hasNextPage;
|
||||||
|
const total: number = response.data.total;
|
||||||
|
|
||||||
|
if (flights.length > 0) {
|
||||||
|
const newFlights: LogbookEntry[] = [...state.flights, ...flights]
|
||||||
|
const newPageIndex: number = state.pageIndex + flights.length;
|
||||||
|
|
||||||
|
dispatch({ type: 'SET_FLIGHTS', payload: { flights: newFlights, hasMoreFlights: hasMore, pageIndex: newPageIndex, totalFlights: total }})
|
||||||
|
|
||||||
|
if (!isUserLoggedIn && flights.length >= 5) {
|
||||||
|
dispatch({ type: 'SET_ALERT', payload: { severity: 'info', message: 'A limited number of flights displayed. Sign in to view all flights.'}})
|
||||||
|
} else {
|
||||||
|
dispatch({ type: 'SET_ALERT', payload: undefined })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dispatch({ type: 'SET_ALERT', payload: { severity: 'info', message: 'No flights found' }})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const axiosError = error as AxiosError;
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_ALERT',
|
||||||
|
payload: { severity: 'error', message: `Loading of flights failed with the following message: ${axiosError.message}`}
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
dispatch({ type: 'SET_IS_LOADING', payload: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadMore = () => {
|
||||||
|
getFlights(state.pageIndex, state.pageSize)
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
getFlights(state.pageIndex, state.pageSize);
|
||||||
const flights: LogbookEntry[] | undefined = logs?.filter((log: LogbookEntry) => {
|
}, [])
|
||||||
if (log.tracks && log.tracks.length > 0) {
|
|
||||||
return log;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (flights && flights.length > 0) {
|
|
||||||
dispatch({ type: 'SET_FLIGHTS', payload: flights})
|
|
||||||
dispatch({ type: 'SET_ALERT', payload: undefined })
|
|
||||||
} else {
|
|
||||||
dispatch({ type: 'SET_ALERT', payload: { severity: 'info', message: 'No flights found' }})
|
|
||||||
}
|
|
||||||
}, [logs])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='max-w-screen-lg mx-auto'>
|
<div className={`max-w-screen-lg mx-auto ${screenSize === ScreenSize.SM || screenSize === ScreenSize.MD? 'mr-4 ml-4' : ''}`}>
|
||||||
<div className='prose mt-5 mb-5'>
|
<div className='prose mt-5 mb-5'>
|
||||||
<h1>Flights</h1>
|
<h1>Flights</h1>
|
||||||
</div>
|
</div>
|
||||||
{!logsLoading && state.alert && (
|
{!state.isLoading && state.alert && (
|
||||||
<div>
|
<div>
|
||||||
<Alert
|
<Alert
|
||||||
className='mb-5'
|
className='mb-5'
|
||||||
@@ -43,14 +79,21 @@ const Flights = () => {
|
|||||||
</Alert>
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!logsLoading &&
|
{!state.isLoading &&
|
||||||
<div>
|
<div>
|
||||||
<LogbookCard logs={state.flights} mode='flights' />
|
<div>
|
||||||
|
<LogbookCard logs={state.flights} mode='flights' />
|
||||||
|
</div>
|
||||||
|
{state.hasMoreFlights &&
|
||||||
|
<div className='flex items-center justify-center mb-5'>
|
||||||
|
<button className='btn btn-link' onClick={loadMore}>Load more</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{logsLoading && [...Array(5)].map((_element, index) => {
|
{state.isLoading && [...Array(5)].map((_element, index) => {
|
||||||
return (
|
return (
|
||||||
<div className='card bg-base-100 border border-base-300 p-2 mb-7'>
|
<div className='card bg-base-100 border border-base-300 p-2 mb-5'>
|
||||||
<div className='card-body' key={index}>
|
<div className='card-body' key={index}>
|
||||||
<div className='skeleton h-10 w-[150px]' />
|
<div className='skeleton h-10 w-[150px]' />
|
||||||
<div className='skeleton h-[350px]' />
|
<div className='skeleton h-[350px]' />
|
||||||
|
|||||||
@@ -5,4 +5,8 @@ export interface FlightsState {
|
|||||||
alert: Alert | undefined;
|
alert: Alert | undefined;
|
||||||
flights: LogbookEntry[];
|
flights: LogbookEntry[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
hasMoreFlights: boolean;
|
||||||
|
pageIndex: number;
|
||||||
|
pageSize: number
|
||||||
|
totalFlights: number;
|
||||||
}
|
}
|
||||||
@@ -5,13 +5,17 @@ import { FlightsState } from "./FlightsState.interface";
|
|||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
| { type: 'SET_ALERT'; payload: Alert | undefined }
|
| { type: 'SET_ALERT'; payload: Alert | undefined }
|
||||||
| { type: 'SET_FLIGHTS'; payload: LogbookEntry[] }
|
| { type: 'SET_FLIGHTS'; payload: { flights: LogbookEntry[], hasMoreFlights: boolean, pageIndex: number, totalFlights: number } }
|
||||||
| { type: 'SET_IS_LOADING'; payload: boolean }
|
| { type: 'SET_IS_LOADING'; payload: boolean }
|
||||||
|
|
||||||
export const initialState: FlightsState = {
|
export const initialState: FlightsState = {
|
||||||
alert: undefined,
|
alert: undefined,
|
||||||
flights: [],
|
flights: [],
|
||||||
isLoading: true
|
isLoading: true,
|
||||||
|
hasMoreFlights: true,
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 5,
|
||||||
|
totalFlights: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reducer = (
|
export const reducer = (
|
||||||
@@ -28,7 +32,10 @@ export const reducer = (
|
|||||||
case 'SET_FLIGHTS': {
|
case 'SET_FLIGHTS': {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
flights: action.payload
|
flights: action.payload.flights,
|
||||||
|
hasMoreFlights: action.payload.hasMoreFlights,
|
||||||
|
pageIndex: action.payload.pageIndex,
|
||||||
|
totalFlights: action.payload.totalFlights
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'SET_IS_LOADING': {
|
case 'SET_IS_LOADING': {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const LogForm = () => {
|
|||||||
`api/logs/${logbookContext.state.selectedLogId}`
|
`api/logs/${logbookContext.state.selectedLogId}`
|
||||||
);
|
);
|
||||||
const log = response.data;
|
const log = response.data;
|
||||||
console.log(log)
|
|
||||||
reset(log);
|
reset(log);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const axiosError = error as AxiosError;
|
const axiosError = error as AxiosError;
|
||||||
@@ -53,11 +53,16 @@ const LogForm = () => {
|
|||||||
if (pilots && FormMode.ADD) {
|
if (pilots && FormMode.ADD) {
|
||||||
const newPilotsOptions = pilots.map((pilot) => {
|
const newPilotsOptions = pilots.map((pilot) => {
|
||||||
return {
|
return {
|
||||||
key: pilot.id,
|
|
||||||
label: pilot.name,
|
label: pilot.name,
|
||||||
|
value: pilot.id,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
newPilotsOptions.unshift({
|
||||||
|
label: '',
|
||||||
|
value: '',
|
||||||
|
})
|
||||||
|
|
||||||
dispatch({ type: 'SET_PILOT_OPTIONS', payload: newPilotsOptions });
|
dispatch({ type: 'SET_PILOT_OPTIONS', payload: newPilotsOptions });
|
||||||
}
|
}
|
||||||
}, [pilots]);
|
}, [pilots]);
|
||||||
@@ -80,9 +85,9 @@ const LogForm = () => {
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={[value]}
|
value={[value]}
|
||||||
>
|
>
|
||||||
{state.pilotOptions?.map((pilotOption: { key: string; label: string, }) => {
|
{state.pilotOptions?.map((pilotOption: { label: string, value: string; }) => {
|
||||||
return (
|
return (
|
||||||
<option key={pilotOption.key}>{pilotOption.label}</option>
|
<option value={pilotOption.value}>{pilotOption.label}</option>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ export interface LogFormState {
|
|||||||
alert: Alert | undefined;
|
alert: Alert | undefined;
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
pilotOptions: { key: string; label: string; }[];
|
pilotOptions: { label: string; value: string; }[];
|
||||||
selectedPilotName: string;
|
selectedPilotName: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ type Action =
|
|||||||
| { type: 'SET_ALERT'; payload: Alert | undefined }
|
| { type: 'SET_ALERT'; payload: Alert | undefined }
|
||||||
| { type: 'SET_IS_DISABLED'; payload: boolean }
|
| { type: 'SET_IS_DISABLED'; payload: boolean }
|
||||||
| { type: 'SET_IS_LOADING'; payload: boolean }
|
| { type: 'SET_IS_LOADING'; payload: boolean }
|
||||||
| { type: 'SET_PILOT_OPTIONS'; payload: { key: string, label: string; }[] }
|
| { type: 'SET_PILOT_OPTIONS'; payload: { label: string; value: string }[] }
|
||||||
| { type: 'SET_SELECTED_PILOT_NAME'; payload: string };
|
| { type: 'SET_SELECTED_PILOT_NAME'; payload: string };
|
||||||
|
|
||||||
export const initialState: LogFormState = {
|
export const initialState: LogFormState = {
|
||||||
|
|||||||
@@ -12,19 +12,40 @@ import { UserRole } from '../../enums/userRole';
|
|||||||
import { useBreakpoints } from '../../hooks/useBreakpoints/UseBreakpoints';
|
import { useBreakpoints } from '../../hooks/useBreakpoints/UseBreakpoints';
|
||||||
import { ScreenSize } from '../../enums/screenSize';
|
import { ScreenSize } from '../../enums/screenSize';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faAdd, faEllipsisVertical, faPen, faEye, faTrash, faMapLocationDot } from '@fortawesome/free-solid-svg-icons'
|
import { faAdd, faEllipsisVertical, faPen, faEye, faTrash, faAngleLeft, faAngleRight, faAnglesLeft, faAnglesRight } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { CellContext, ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, HeaderContext, useReactTable } from '@tanstack/react-table';
|
import { CellContext, ColumnDef, flexRender, getCoreRowModel, HeaderContext, PaginationState, useReactTable } from '@tanstack/react-table';
|
||||||
import { useLogbookContext } from '../../hooks/logbookContext/UseLogbookContext';
|
import { useLogbookContext } from '../../hooks/logbookContext/UseLogbookContext';
|
||||||
import LogbookDrawer from '../logbookDrawer/LogbookDrawer';
|
import LogbookDrawer from '../logbookDrawer/LogbookDrawer';
|
||||||
import Alert from '../alert/Alert';
|
import Alert from '../alert/Alert';
|
||||||
|
import TrackMap from '../trackMap/TrackMap';
|
||||||
|
|
||||||
|
interface ActionsProps {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
const Logbook: React.FC<unknown> = () => {
|
const Logbook: React.FC<unknown> = () => {
|
||||||
const [state, dispatch] = useReducer(reducer, initialState);
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
const [columnVisibility, setColumnVisibility] = useState({})
|
const [columnVisibility, setColumnVisibility] = useState({});
|
||||||
const logbookContext = useLogbookContext()
|
const logbookContext = useLogbookContext();
|
||||||
const { isUserLoggedIn } = useOidc();
|
const { isUserLoggedIn } = useOidc();
|
||||||
const { userRole } = useUserRole();
|
const { userRole } = useUserRole();
|
||||||
const { screenSize } = useBreakpoints();
|
const { screenSize } = useBreakpoints();
|
||||||
|
const Actions = ({ id }: ActionsProps) => {
|
||||||
|
return (
|
||||||
|
<div className='dropdown dropdown-end'>
|
||||||
|
<div tabIndex={0} role='button' className='btn btn-ghost p-0'><FontAwesomeIcon icon={faEllipsisVertical} /></div>
|
||||||
|
<ul tabIndex={-1} className="dropdown-content menu bg-base-100 rounded-box w-52 p-2 shadow-sm border border-base-300 !z-[100]">
|
||||||
|
{isUserLoggedIn && userRole === UserRole.WRITE &&
|
||||||
|
<li><a onClick={() => onOpenCloseDrawer(FormMode.EDIT, id)}><FontAwesomeIcon icon={faPen} />Edit</a></li>
|
||||||
|
}
|
||||||
|
<li><a onClick={() => onOpenCloseDrawer(FormMode.VIEW, id)}><FontAwesomeIcon icon={faEye} />View</a></li>
|
||||||
|
{isUserLoggedIn && userRole === UserRole.WRITE &&
|
||||||
|
<li><a onClick={() => onDeleteLog(id)}><FontAwesomeIcon icon={faTrash} />Delete</a></li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
const columnTotal = (info: HeaderContext<LogbookEntry, unknown>): number => {
|
const columnTotal = (info: HeaderContext<LogbookEntry, unknown>): number => {
|
||||||
const values: number[] = info.table.getPaginationRowModel().rows.map((row: any) => Number(row.getValue(info.column.id))).filter((value: any) => !Number.isNaN(value));
|
const values: number[] = info.table.getPaginationRowModel().rows.map((row: any) => Number(row.getValue(info.column.id))).filter((value: any) => !Number.isNaN(value));
|
||||||
let total: number = 0;
|
let total: number = 0;
|
||||||
@@ -116,18 +137,7 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
},
|
},
|
||||||
cell: (info: CellContext<LogbookEntry, unknown>) => {
|
cell: (info: CellContext<LogbookEntry, unknown>) => {
|
||||||
return (
|
return (
|
||||||
<div className='dropdown dropdown-end'>
|
<Actions id={info.row.original.id} />
|
||||||
<div tabIndex={0} role='button' className='btn btn-ghost'><FontAwesomeIcon icon={faEllipsisVertical} /></div>
|
|
||||||
<ul tabIndex={-1} className="dropdown-content menu bg-base-100 rounded-box w-52 p-2 shadow-sm border border-base-300 !z-[100]">
|
|
||||||
{isUserLoggedIn && userRole === UserRole.WRITE &&
|
|
||||||
<li><a onClick={() => onOpenCloseDrawer(FormMode.EDIT, info.row.original.id)}><FontAwesomeIcon icon={faPen} />Edit</a></li>
|
|
||||||
}
|
|
||||||
<li><a onClick={() => onOpenCloseDrawer(FormMode.VIEW, info.row.original.id)}><FontAwesomeIcon icon={faEye} />View</a></li>
|
|
||||||
{isUserLoggedIn && userRole === UserRole.WRITE &&
|
|
||||||
<li><a onClick={() => onDeleteLog(info.row.original.id)}><FontAwesomeIcon icon={faTrash} />Delete</a></li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -338,25 +348,51 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
actions
|
actions
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const onPaginationChange = (updater: any) => {
|
||||||
|
const newPaginationState: PaginationState = typeof updater === 'function' ? updater(state.pagination) : updater;
|
||||||
|
|
||||||
|
dispatch({ type: 'SET_PAGINATION', payload: newPaginationState })
|
||||||
|
}
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: state.entries,
|
data: state.entries,
|
||||||
columns: columns,
|
columns: columns,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
manualPagination: true,
|
||||||
onColumnVisibilityChange: setColumnVisibility,
|
onColumnVisibilityChange: setColumnVisibility,
|
||||||
|
onPaginationChange: onPaginationChange,
|
||||||
|
rowCount: state.totalEntries,
|
||||||
state: {
|
state: {
|
||||||
columnVisibility: columnVisibility
|
columnVisibility: columnVisibility,
|
||||||
|
pagination: state.pagination
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const getLogbookEntries = async () => {
|
const {
|
||||||
|
firstPage,
|
||||||
|
getCanNextPage,
|
||||||
|
getCanPreviousPage,
|
||||||
|
getPageCount,
|
||||||
|
getState,
|
||||||
|
lastPage,
|
||||||
|
nextPage,
|
||||||
|
previousPage,
|
||||||
|
setPageIndex
|
||||||
|
} = table;
|
||||||
|
|
||||||
|
const getLogbookEntries = async (pageIndex: number, pageSize: number) => {
|
||||||
try {
|
try {
|
||||||
dispatch({ type: 'SET_IS_LOADING', payload: true });
|
dispatch({ type: 'SET_IS_LOADING', payload: true });
|
||||||
|
|
||||||
const response: AxiosResponse = await httpClient.get(`api/logs`);
|
const response: AxiosResponse = await httpClient.get(`api/logs`, {
|
||||||
|
params: {
|
||||||
|
skip: pageIndex * pageSize,
|
||||||
|
take: pageSize
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (response.data.length > 0) {
|
if (response.data.entities.length > 0) {
|
||||||
const entries: LogbookEntry[] = response.data;
|
const entries: LogbookEntry[] = response.data.entities;
|
||||||
const entryColumns: string[] = Object.keys(entries[0]);
|
const entryColumns: string[] = Object.keys(entries[0]);
|
||||||
const columnVisibility: {[key: string]: boolean} = {}
|
const columnVisibility: {[key: string]: boolean} = {}
|
||||||
|
|
||||||
@@ -375,9 +411,11 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
|
|
||||||
entries.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
entries.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||||
setColumnVisibility(columnVisibility)
|
setColumnVisibility(columnVisibility)
|
||||||
dispatch({ type: 'SET_ENTRIES', payload: entries });
|
dispatch({ type: 'SET_ENTRIES', payload: { entries: entries, totalEntries: response.data.total }});
|
||||||
|
|
||||||
if (state.alert) {
|
if (!isUserLoggedIn && response.data.entities.length >= 5) {
|
||||||
|
dispatch({ type: 'SET_ALERT', payload: { severity: 'info', message: 'A limited number of log entries displayed. Sign in to view all log entries.'}})
|
||||||
|
} else {
|
||||||
dispatch({ type: 'SET_ALERT', payload: undefined})
|
dispatch({ type: 'SET_ALERT', payload: undefined})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -440,7 +478,7 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
payload: false
|
payload: false
|
||||||
});
|
});
|
||||||
logbookContext.dispatch({ type: 'SET_SELECTED_LOG_ID', payload: '' })
|
logbookContext.dispatch({ type: 'SET_SELECTED_LOG_ID', payload: '' })
|
||||||
await getLogbookEntries();
|
await getLogbookEntries(state.pagination?.pageIndex, state.pagination?.pageSize);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const axiosError = error as AxiosError;
|
const axiosError = error as AxiosError;
|
||||||
|
|
||||||
@@ -460,19 +498,46 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onRowsPerPageChanged = (event: any) => {
|
||||||
|
const newPaginationState: PaginationState = {
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: event.target.value !== 'All' ? Number(event.target.value) : state.totalEntries
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch({ type: 'SET_PAGINATION', payload: newPaginationState })
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!logbookContext.state.isDrawerOpen) {
|
if (!logbookContext.state.isDrawerOpen) {
|
||||||
getLogbookEntries();
|
getLogbookEntries(state.pagination.pageIndex, state.pagination.pageSize);
|
||||||
}
|
}
|
||||||
}, [logbookContext.state.isDrawerOpen]);
|
}, [logbookContext.state.isDrawerOpen, state.pagination.pageIndex, state.pagination.pageSize]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const pages: number[] = [];
|
||||||
|
|
||||||
|
if (state.pagination?.pageSize) {
|
||||||
|
const totalPages = getPageCount();
|
||||||
|
|
||||||
|
for(let i = 0; i < totalPages; i++) {
|
||||||
|
pages.push(i + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({ type: 'SET_PAGES', payload: pages })
|
||||||
|
}, [state.totalEntries, state.pagination?.pageSize])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(state.alert)
|
||||||
|
}, [state.alert])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='mr-10 ml-10 grid grid-cols-12'>
|
<div className={`${screenSize === ScreenSize.SM ? 'mr-4 ml-4' : 'mr-10 ml-10'} grid grid-cols-12`}>
|
||||||
<div className='prose max-w-none col-span-10 mt-5 mb-5'>
|
<div className='prose max-w-none col-span-6 mt-5 mb-5'>
|
||||||
<h1>Logbook</h1>
|
<h1>Logbook</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className='col-span-2 justify-self-end self-center'>
|
<div className='col-span-6 justify-self-end self-center'>
|
||||||
{userRole === UserRole.WRITE &&
|
{userRole === UserRole.WRITE &&
|
||||||
<button className='btn btn-primary'
|
<button className='btn btn-primary'
|
||||||
onClick={() => onOpenCloseDrawer(FormMode.ADD)}
|
onClick={() => onOpenCloseDrawer(FormMode.ADD)}
|
||||||
@@ -489,15 +554,31 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
onClose={() =>
|
onClose={() =>
|
||||||
dispatch({ type: 'SET_ALERT', payload: undefined })
|
dispatch({ type: 'SET_ALERT', payload: undefined })
|
||||||
}
|
}
|
||||||
severity={'info'}
|
severity={state.alert.severity}
|
||||||
>
|
>
|
||||||
{state.alert.message}
|
{state.alert.message}
|
||||||
</Alert>
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!state.isLoading && state.entries.length > 0 && screenSize !== ScreenSize.SM && (
|
{!state.isLoading && state.entries.length > 0 && screenSize !== ScreenSize.SM && screenSize !== ScreenSize.MD && (
|
||||||
<div className='col-span-12 p-5 bg-base-100 border border-base-100 rounded-lg'>
|
<div className='col-span-12 pr-5 pb-5 pl-5 bg-base-100 border border-base-100 rounded-lg '>
|
||||||
<div className='overflow-x-auto'>
|
<div className='overflow-x-auto mb-5'>
|
||||||
|
<div className='col-span-12 justify-self-end self-center mt-1 mr-1 mb-2'>
|
||||||
|
<label className='select select-sm select-ghost'>
|
||||||
|
<span className='label'>Rows per page</span>
|
||||||
|
<select
|
||||||
|
defaultValue={1}
|
||||||
|
onChange={onRowsPerPageChanged}
|
||||||
|
value={state.pagination?.pageSize}
|
||||||
|
>
|
||||||
|
<option value={10}>10</option>
|
||||||
|
<option value={25}>25</option>
|
||||||
|
<option value={50}>50</option>
|
||||||
|
<option value={state.totalEntries}>All</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
</div>
|
||||||
<table className='table min-w-full h-auto table-auto w-full'>
|
<table className='table min-w-full h-auto table-auto w-full'>
|
||||||
<thead className='bg-base-200'>
|
<thead className='bg-base-200'>
|
||||||
{table.getHeaderGroups().map((headerGroup, headerGroupIndex) => (
|
{table.getHeaderGroups().map((headerGroup, headerGroupIndex) => (
|
||||||
@@ -583,10 +664,92 @@ const Logbook: React.FC<unknown> = () => {
|
|||||||
</thead>
|
</thead>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
{state.pagination.pageSize !== state.totalEntries &&
|
||||||
|
<div className='col-span-12 justify-self-center self-center'>
|
||||||
|
<div className='join'>
|
||||||
|
<button
|
||||||
|
className='join-item btn btn-sm'
|
||||||
|
onClick={firstPage}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faAnglesLeft} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className='join-item btn btn-sm'
|
||||||
|
onClick={previousPage}
|
||||||
|
disabled={!getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faAngleLeft} />
|
||||||
|
</button>
|
||||||
|
{state.pages.map((page, index) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={`join-item btn btn-sm ${getState().pagination.pageIndex === index ? 'btn-active' : ''}`}
|
||||||
|
onClick={() => setPageIndex(index)}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<button
|
||||||
|
className='join-item btn btn-sm'
|
||||||
|
onClick={() => nextPage()}
|
||||||
|
disabled={!getCanNextPage()}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faAngleRight} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className='join-item btn btn-sm'
|
||||||
|
onClick={lastPage}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faAnglesRight} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{state.entries.length > 0 && screenSize === ScreenSize.SM &&
|
{state.entries.length > 0 && screenSize === ScreenSize.SM &&
|
||||||
<LogbookCard logs={state.entries} onDelete={onDeleteLog} mode='logbook' onOpenCloseForm={onOpenCloseDrawer} />
|
<div className='col-span-12'>
|
||||||
|
<>
|
||||||
|
{table.getRowModel().rows.map((row) => {
|
||||||
|
const date = new Date(row.original.date.replace('Z', ''));
|
||||||
|
const formattedDate: string = `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='card bg-base-100 border border-base-300 mb-5'>
|
||||||
|
<div className={`card-body ${screenSize === ScreenSize.SM || screenSize === ScreenSize.MD ? 'p-4' : ''}`} key={row.id}>
|
||||||
|
<div className={`grid grid-cols-12 gap-3`}>
|
||||||
|
<>
|
||||||
|
<div className='col-span-8'>
|
||||||
|
<h2 className='card-title font-bold text-2xl'>{formattedDate}</h2>
|
||||||
|
</div>
|
||||||
|
<div className='col-span-4 justify-self-end self-center'>
|
||||||
|
<Actions id={row.original.id} />
|
||||||
|
</div>
|
||||||
|
{row.getVisibleCells().map((cell) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{cell.column.columnDef.header !== 'Actions' && cell.column.columnDef.header !== 'Date' && cell.column.columnDef.header !== 'Pilot' &&
|
||||||
|
<>
|
||||||
|
<div className='col-span-8 font-bold'>
|
||||||
|
<span>{cell.getContext().column.columnDef.header?.toString()}</span>
|
||||||
|
</div>
|
||||||
|
<div className='col-span-4'>
|
||||||
|
<span>{flexRender(cell.column.columnDef.cell, cell.getContext())}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
{state.isLoading && !state.alert && (
|
{state.isLoading && !state.alert && (
|
||||||
<div className='col-span-12 p-5 bg-base-100 border border-base-100 rounded-lg'>
|
<div className='col-span-12 p-5 bg-base-100 border border-base-100 rounded-lg'>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef, PaginationState } from '@tanstack/react-table';
|
||||||
import { FormMode } from '../../enums/formMode';
|
import { FormMode } from '../../enums/formMode';
|
||||||
import { Alert } from '../../interfaces/Alert.interface';
|
import { Alert } from '../../interfaces/Alert.interface';
|
||||||
import { LogbookEntry } from './LogbookEntry.interface';
|
import { LogbookEntry } from './LogbookEntry.interface';
|
||||||
@@ -10,4 +10,7 @@ export interface LogbookState {
|
|||||||
isConfirmDialogLoading: boolean;
|
isConfirmDialogLoading: boolean;
|
||||||
isConfirmDialogOpen: boolean;
|
isConfirmDialogOpen: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
pages: number[];
|
||||||
|
pagination: PaginationState;
|
||||||
|
totalEntries: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef, PaginationState } from '@tanstack/react-table';
|
||||||
import { FormMode } from '../../enums/formMode';
|
import { FormMode } from '../../enums/formMode';
|
||||||
import { Alert } from '../../interfaces/Alert.interface';
|
import { Alert } from '../../interfaces/Alert.interface';
|
||||||
import { LogbookEntry } from './LogbookEntry.interface';
|
import { LogbookEntry } from './LogbookEntry.interface';
|
||||||
@@ -7,11 +7,12 @@ import { LogbookState } from './LogbookState.interface';
|
|||||||
type Action =
|
type Action =
|
||||||
| { type: 'SET_COLUMNS'; payload: ColumnDef<LogbookEntry>[] }
|
| { type: 'SET_COLUMNS'; payload: ColumnDef<LogbookEntry>[] }
|
||||||
| { type: 'SET_IS_CONFIRMATION_DIALOG_OPEN'; payload: boolean }
|
| { type: 'SET_IS_CONFIRMATION_DIALOG_OPEN'; payload: boolean }
|
||||||
| { type: 'SET_ENTRIES'; payload: LogbookEntry[] }
|
| { type: 'SET_ENTRIES'; payload: { entries: LogbookEntry[], totalEntries: number } }
|
||||||
| { type: 'SET_ALERT'; payload: Alert | undefined }
|
| { type: 'SET_ALERT'; payload: Alert | undefined }
|
||||||
| { type: 'SET_IS_CONFIRMATION_DIALOG_LOADING'; payload: boolean }
|
| { type: 'SET_IS_CONFIRMATION_DIALOG_LOADING'; payload: boolean }
|
||||||
| { type: 'SET_IS_LOADING'; payload: boolean };
|
| { type: 'SET_IS_LOADING'; payload: boolean }
|
||||||
|
| { type: 'SET_PAGES'; payload: number[] }
|
||||||
|
| { type: 'SET_PAGINATION'; payload: PaginationState };
|
||||||
|
|
||||||
export const initialState: LogbookState = {
|
export const initialState: LogbookState = {
|
||||||
alert: undefined,
|
alert: undefined,
|
||||||
@@ -19,7 +20,13 @@ export const initialState: LogbookState = {
|
|||||||
entries: [],
|
entries: [],
|
||||||
isConfirmDialogLoading: false,
|
isConfirmDialogLoading: false,
|
||||||
isConfirmDialogOpen: false,
|
isConfirmDialogOpen: false,
|
||||||
isLoading: false
|
isLoading: false,
|
||||||
|
pages: [],
|
||||||
|
pagination: {
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 10
|
||||||
|
},
|
||||||
|
totalEntries: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
export const reducer = (
|
export const reducer = (
|
||||||
@@ -42,7 +49,8 @@ export const reducer = (
|
|||||||
case 'SET_ENTRIES': {
|
case 'SET_ENTRIES': {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
entries: action.payload
|
entries: action.payload.entries,
|
||||||
|
totalEntries: action.payload.totalEntries
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'SET_ALERT': {
|
case 'SET_ALERT': {
|
||||||
@@ -63,6 +71,18 @@ export const reducer = (
|
|||||||
isLoading: action.payload
|
isLoading: action.payload
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case 'SET_PAGES': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
pages: action.payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'SET_PAGINATION': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
pagination: action.payload
|
||||||
|
}
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { LogbookCardProps } from "./LogbookCardProps.interface";
|
import { LogbookCardProps } from "./LogbookCardProps.interface";
|
||||||
import TrackMap from "../trackMap/TrackMap";
|
import TrackMap from "../trackMap/TrackMap";
|
||||||
|
import { useBreakpoints } from "../../hooks/useBreakpoints/UseBreakpoints";
|
||||||
|
import { ScreenSize } from "../../enums/screenSize";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
const LogbookCard = ({ logs, mode, onDelete, onOpenCloseForm }: LogbookCardProps) => {
|
const LogbookCard = ({ logs, mode, onDelete, onOpenCloseForm }: LogbookCardProps) => {
|
||||||
|
const { screenSize } = useBreakpoints();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{logs.map((log) => {
|
{logs.map((log) => {
|
||||||
@@ -9,8 +14,8 @@ const LogbookCard = ({ logs, mode, onDelete, onOpenCloseForm }: LogbookCardProps
|
|||||||
const formattedDate: string = `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`;
|
const formattedDate: string = `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='card bg-base-100 border border-base-300 p-2'>
|
<div className='card bg-base-100 border border-base-300 p-2 mb-5'>
|
||||||
<div className='card-body' key={log.id}>
|
<div className={`card-body ${screenSize === ScreenSize.SM || screenSize === ScreenSize.MD ? 'p-2' : ''}`} key={log.id}>
|
||||||
<h2 className='card-title font-bold text-2xl'>{formattedDate}</h2>
|
<h2 className='card-title font-bold text-2xl'>{formattedDate}</h2>
|
||||||
<div>
|
<div>
|
||||||
{mode === 'flights' && log.tracks && log.tracks.length > 0 &&
|
{mode === 'flights' && log.tracks && log.tracks.length > 0 &&
|
||||||
@@ -24,10 +29,10 @@ const LogbookCard = ({ logs, mode, onDelete, onOpenCloseForm }: LogbookCardProps
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div className='collapse collapse-arrow bg-base-100 border-base-300 border'>
|
<div className='collapse collapse-arrow bg-base-100 border-base-300 border'>
|
||||||
<input type='radio' name='details-accordion' />
|
<input type='checkbox' />
|
||||||
<div className='collapse-title font-semibold'>Details</div>
|
<div className='collapse-title font-semibold'>Details</div>
|
||||||
<div className='collapse-content'>
|
<div className='collapse-content'>
|
||||||
<div className='grid grid-cols-12 gap-3 mr-[30%] ml-[30%] mt-4 mb-4'>
|
<div className={`grid grid-cols-12 gap-3 ${screenSize === ScreenSize.SM || screenSize === ScreenSize.MD ? 'mr-[5%] ml-[5%]' : 'mr-[30%] ml-[30%]'} mt-4 mb-4`}>
|
||||||
<div className='col-span-6 font-bold'>
|
<div className='col-span-6 font-bold'>
|
||||||
<span>Aircraft Make and Model</span>
|
<span>Aircraft Make and Model</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,11 +17,31 @@ import { Pilot } from './Pilot.interface';
|
|||||||
import Alert from '../alert/Alert';
|
import Alert from '../alert/Alert';
|
||||||
import { useOidc } from '../../auth/oidcConfig';
|
import { useOidc } from '../../auth/oidcConfig';
|
||||||
|
|
||||||
|
interface ActionsProps {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
const Pilots: React.FC<unknown> = () => {
|
const Pilots: React.FC<unknown> = () => {
|
||||||
const [state, dispatch] = useReducer(reducer, initialState);
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
const { userRole } = useUserRole();
|
const { userRole } = useUserRole();
|
||||||
const { screenSize } = useBreakpoints();
|
const { screenSize } = useBreakpoints();
|
||||||
const { isUserLoggedIn } = useOidc();
|
const { isUserLoggedIn } = useOidc();
|
||||||
|
const Actions = ({ id }: ActionsProps) => {
|
||||||
|
return (
|
||||||
|
<div className='dropdown dropdown-end'>
|
||||||
|
<div tabIndex={0} role='button' className='btn btn-ghost'><FontAwesomeIcon icon={faEllipsisVertical} /></div>
|
||||||
|
<ul tabIndex={-1} className="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm border border-base-300">
|
||||||
|
{isUserLoggedIn && userRole === UserRole.WRITE &&
|
||||||
|
<li><a onClick={() => onOpenClosePilotForm(FormMode.EDIT, id)}><FontAwesomeIcon icon={faPen} />Edit</a></li>
|
||||||
|
}
|
||||||
|
<li><a onClick={() => onOpenClosePilotForm(FormMode.VIEW, id)}><FontAwesomeIcon icon={faEye} />View</a></li>
|
||||||
|
{isUserLoggedIn && userRole === UserRole.WRITE &&
|
||||||
|
<li><a onClick={() => onDeletePilot(id)}><FontAwesomeIcon icon={faTrash} />Delete</a></li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const getPilots = async () => {
|
const getPilots = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -134,18 +154,7 @@ const Pilots: React.FC<unknown> = () => {
|
|||||||
},
|
},
|
||||||
cell: (info: CellContext<Pilot, unknown>) => {
|
cell: (info: CellContext<Pilot, unknown>) => {
|
||||||
return (
|
return (
|
||||||
<div className='dropdown dropdown-end'>
|
<Actions id={info.row.original.id} />
|
||||||
<div tabIndex={0} role='button' className='btn btn-ghost'><FontAwesomeIcon icon={faEllipsisVertical} /></div>
|
|
||||||
<ul tabIndex={-1} className="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm border border-base-300">
|
|
||||||
{isUserLoggedIn && userRole === UserRole.WRITE &&
|
|
||||||
<li><a onClick={() => onOpenClosePilotForm(FormMode.EDIT, info.row.original.id)}><FontAwesomeIcon icon={faPen} />Edit</a></li>
|
|
||||||
}
|
|
||||||
<li><a onClick={() => onOpenClosePilotForm(FormMode.VIEW, info.row.original.id)}><FontAwesomeIcon icon={faEye} />View</a></li>
|
|
||||||
{isUserLoggedIn && userRole === UserRole.WRITE &&
|
|
||||||
<li><a onClick={() => onDeletePilot(info.row.original.id)}><FontAwesomeIcon icon={faTrash} />Delete</a></li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,11 +181,11 @@ const Pilots: React.FC<unknown> = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='mr-10 ml-10 grid grid-cols-12'>
|
<div className={`${screenSize === ScreenSize.SM ? 'mr-4 ml-4' : 'mr-10 ml-10'} grid grid-cols-12`}>
|
||||||
<div className='prose max-w-none col-span-10 mt-5 mb-5' >
|
<div className='prose max-w-none col-span-6 mt-5 mb-5' >
|
||||||
<h1>Pilots</h1>
|
<h1>Pilots</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className='col-span-2 justify-self-end self-center'>
|
<div className='col-span-6 justify-self-end self-center'>
|
||||||
{!state.isLoading && userRole === UserRole.WRITE &&
|
{!state.isLoading && userRole === UserRole.WRITE &&
|
||||||
<button
|
<button
|
||||||
className='btn btn-primary'
|
className='btn btn-primary'
|
||||||
@@ -259,9 +268,40 @@ const Pilots: React.FC<unknown> = () => {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{/* {state.pilots.length > 0 && screenSize === ScreenSize.SM &&
|
{state.pilots.length > 0 && screenSize === ScreenSize.SM &&
|
||||||
<PilotCard pilots={state.pilots} onDelete={onDeletePilot} onOpenCloseForm={onOpenClosePilotForm} />
|
<div className='col-span-12'>
|
||||||
} */}
|
<>
|
||||||
|
{table.getRowModel().rows.map((row) => {
|
||||||
|
return (
|
||||||
|
<div className='card bg-base-100 border border-base-300 mb-5'>
|
||||||
|
<div className={`card-body ${screenSize === ScreenSize.SM || screenSize === ScreenSize.MD ? 'p-4' : ''}`} key={row.id}>
|
||||||
|
<div className={`grid grid-cols-12 gap-3`}>
|
||||||
|
<>
|
||||||
|
{row.getVisibleCells().map((cell) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{cell.column.columnDef.header !== 'Actions' &&
|
||||||
|
<>
|
||||||
|
<div className='col-span-10 self-center'>
|
||||||
|
<span>{flexRender(cell.column.columnDef.cell, cell.getContext())}</span>
|
||||||
|
</div>
|
||||||
|
<div className='col-span-2'>
|
||||||
|
<Actions id={row.original.id} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
{state.isFormOpen && (
|
{state.isFormOpen && (
|
||||||
<PilotForm
|
<PilotForm
|
||||||
|
|||||||
Binary file not shown.
3
infrastructure/.gitignore
vendored
3
infrastructure/.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
# Terraform
|
# Terraform
|
||||||
.terraform/*
|
.terraform/*
|
||||||
*.tfstate
|
*.tfstate
|
||||||
*.tfstate.*
|
*.tfstate.*
|
||||||
|
.DS_Store
|
||||||
@@ -60,7 +60,7 @@ resource "azurerm_container_app" "container_app" {
|
|||||||
|
|
||||||
container {
|
container {
|
||||||
cpu = 0.25
|
cpu = 0.25
|
||||||
image = "noahspan/flying:20588458616"
|
image = "noahspan/flying:20589367895"
|
||||||
memory = "0.5Gi"
|
memory = "0.5Gi"
|
||||||
name = "flying"
|
name = "flying"
|
||||||
|
|
||||||
@@ -99,6 +99,11 @@ resource "azurerm_container_app" "container_app" {
|
|||||||
value = var.JWKS_URI
|
value = var.JWKS_URI
|
||||||
}
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "NODE_ENV"
|
||||||
|
value = "test"
|
||||||
|
}
|
||||||
|
|
||||||
env {
|
env {
|
||||||
name = "SESSION_SECRET"
|
name = "SESSION_SECRET"
|
||||||
secret_name = "session-secret"
|
secret_name = "session-secret"
|
||||||
@@ -116,13 +121,13 @@ resource "azurerm_container_app" "container_app" {
|
|||||||
|
|
||||||
env {
|
env {
|
||||||
name = "DB_SYNC"
|
name = "DB_SYNC"
|
||||||
value = "false"
|
value = "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
startup_probe {
|
startup_probe {
|
||||||
failure_count_threshold = 10
|
failure_count_threshold = 3
|
||||||
initial_delay = 1
|
initial_delay = 15
|
||||||
interval_seconds = 2
|
interval_seconds = 30
|
||||||
path = "/api/health"
|
path = "/api/health"
|
||||||
port = 3000
|
port = 3000
|
||||||
transport = "HTTP"
|
transport = "HTTP"
|
||||||
|
|||||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@noahspan/flying",
|
"name": "@noahspan/flying",
|
||||||
"version": "2.0.0-alpha-3",
|
"version": "2.0.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@noahspan/flying",
|
"name": "@noahspan/flying",
|
||||||
"version": "2.0.0-alpha-3",
|
"version": "2.0.2",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"api",
|
"api",
|
||||||
"client",
|
"client",
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"api": {
|
"api": {
|
||||||
"version": "2.0.0-alpha-3",
|
"version": "2.0.2",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/storage-blob": "^12.27.0",
|
"@azure/storage-blob": "^12.27.0",
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"client": {
|
"client": {
|
||||||
"version": "2.0.0-alpha-3",
|
"version": "2.0.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@noahspan/flying",
|
"name": "@noahspan/flying",
|
||||||
"version": "2.0.0-beta-4",
|
"version": "2.1.4",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node api/dist/main",
|
"start": "node api/dist/main",
|
||||||
"format": "prettier --write \"**/src/**/*.ts\" \"**/test/**/*.ts\"",
|
"format": "prettier --write \"**/src/**/*.ts\" \"**/test/**/*.ts\"",
|
||||||
|
|||||||
Reference in New Issue
Block a user