Compare commits

...

14 Commits

Author SHA1 Message Date
5e9d3d1e9a Merge branch 'main' into 115-add-load-more-button-to-logbook-mobile-view 2026-03-21 20:55:59 -05:00
26f712b702 fixing logbook page responsiveness 2026-03-21 20:50:47 -05:00
b7b6bdcf4a 115 add load more button to logbook mobile view (#117)
* fixing logbook page responsiveness

* fixing logbook page responsiveness

* fixing logbook page responsiveness
2026-03-21 20:41:50 -05:00
ff8922e86c fixing logbook page responsiveness 2026-03-21 20:39:45 -05:00
9c96774f7b fixing logbook page responsiveness (#116)
* fixing logbook page responsiveness

* fixing logbook page responsiveness
2026-03-21 20:30:05 -05:00
5950cef79e fixing logbook page responsiveness 2026-03-21 20:28:20 -05:00
bde20ff73b fixing logbook page responsiveness 2026-03-21 20:25:21 -05:00
a9abb5863c adding load more button to flights page (#114) 2026-03-21 10:43:44 -05:00
477469831f adding pagination to logbook table (#112) 2026-03-16 07:30:29 -05:00
6964531e12 Update logbook table pagesize (#110)
* updating default logbook table page size

* updating default logbook table page size
2026-03-08 18:05:38 -05:00
08baaa6d83 updating default logbook table page size (#109) 2026-03-08 17:40:10 -05:00
487afb08d6 adding api unit tests (#108)
* adding api unit tests

* adding not signed in alerts to flights and logs
2026-03-08 14:58:24 -05:00
b7e74242b8 minor fixes 2026-01-29 08:56:11 -06:00
876f1f65a2 updating log entity with numeric columns 2026-01-27 12:54:08 -06:00
52 changed files with 2476 additions and 320 deletions

View File

@@ -10,7 +10,7 @@ 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:
@@ -38,6 +38,10 @@ 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 }}

View File

@@ -13,7 +13,7 @@ jobs:
name: build name: build
needs: needs:
- changes - changes
uses: ./.github/workflows/build.yaml uses: ./.github/workflows/build_and_test.yaml
with: with:
environment_name: test environment_name: test
version_number: ${{ github.run_id }} version_number: ${{ github.run_id }}

View File

@@ -12,7 +12,7 @@ jobs:
name: build name: build
needs: needs:
- changes - changes
uses: ./.github/workflows/build.yaml uses: ./.github/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 }}

View File

@@ -7,7 +7,7 @@ on:
jobs: jobs:
build: build:
name: build name: build
uses: ./.github/workflows/build.yaml uses: ./.github/workflows/build_and_test.yaml
with: with:
environment_name: prod environment_name: prod
version_number: ${{ github.ref_name }} version_number: ${{ github.ref_name }}

View File

@@ -1,6 +1,6 @@
{ {
"name": "api", "name": "api",
"version": "2.0.0-beta-2", "version": "2.0.2",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,

View File

@@ -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' })

View File

@@ -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()

View File

@@ -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);
it('isHealth => should return status ok', async () => {
const result = await controller.isHealthy(); const result = await controller.isHealthy();
expect(mockHealthService.isDatabaseConnected).toHaveBeenCalled(); expect(result).toEqual(HttpStatus.OK);
expect(result).toEqual(true);
})
it('isHealthy => should return error', async () => {
jest.spyOn(mockHealthService, 'isDatabaseConnected').mockReturnValue(false);
const result = await controller.isHealthy();
expect(mockHealthService.isDatabaseConnected).toHaveBeenCalled();
expect(result).toEqual(false);
}) })
}) })

View File

@@ -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);
}
} }
} }

View File

@@ -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 {}

View File

@@ -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);
});
});

View File

@@ -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;
}
}
}

View File

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

View 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')
}
})
})

View File

@@ -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,6 +32,29 @@ 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()
@@ -45,27 +70,14 @@ export class LogController {
} }
} }
@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,7 +87,7 @@ 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 {
return await this.logService.update(id, logDto); return await this.logService.update(id, logDto);
} catch (error) { } catch (error) {

View File

@@ -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;

View File

@@ -1,5 +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 '../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' })
@@ -25,52 +26,131 @@ export class LogEntity {
@Column() @Column()
routeTo: string; routeTo: string;
@Column() @Column('numeric', {
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer()
})
durationOfFlight: number; durationOfFlight: number;
@Column({ nullable: true }) @Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
singleEngineLand: number | null; singleEngineLand: number | null;
@Column({ nullable: true }) @Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
simulatorAtd: number | null; simulatorAtd: number | null;
@Column({ nullable: true }) @Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
landingsDay: number | null; landingsDay: number | null;
@Column({ nullable: true }) @Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
landingsNight: number | null; landingsNight: number | null;
@Column({ nullable: true }) @Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
groundTrainingReceived: number | null; groundTrainingReceived: number | null;
@Column({ nullable: true }) @Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
flightTrainingReceived: number | null; flightTrainingReceived: number | null;
@Column({ nullable: true }) @Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
crossCountry: number | null; crossCountry: number | null;
@Column({ nullable: true }) @Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
night: number | null; night: number | null;
@Column({ nullable: true }) @Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
solo: number | null; solo: number | null;
@Column({ nullable: true }) @Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
pilotInCommand: number | null; pilotInCommand: number | null;
@Column({ nullable: true }) @Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
instrumentActual: number | null; instrumentActual: number | null;
@Column({ nullable: true }) @Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
instrumentSimulated: number | null; instrumentSimulated: number | null;
@Column({ nullable: true }) @Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
instrumentApproaches: number | null; instrumentApproaches: number | null;
@Column({ nullable: true }) @Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
instrumentHolds: number | null; instrumentHolds: number | null;
@Column({ nullable: true }) @Column('numeric', {
nullable: true,
precision: 10,
scale: 1,
transformer: new ColumnNumericTransformer(),
})
instrumentNavTrack: number | null; instrumentNavTrack: number | null;
@Column({ nullable: true }) @Column({ nullable: true })

View File

@@ -15,10 +15,9 @@ 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: {
@@ -31,7 +30,6 @@ export class LogInterceptor implements NestInterceptor {
durationOfFlight: log.durationOfFlight, durationOfFlight: log.durationOfFlight,
tracks: log.tracks, tracks: log.tracks,
}; };
});
} }
if (req.headers.authorization) { if (req.headers.authorization) {
@@ -41,17 +39,39 @@ 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(data.entities));
return {
entities: logs,
total: data.total,
hasNextPage: data.hasNextPage
};
} else {
const log = limitData(data);
return log;
}
return logs;
} 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 publicData = data.entities.map((entity) => limitData(entity))
const logs = publicData.slice(0, 5) const logs = publicData.slice(0, 5)
return logs; return {
entities: publicData,
total: data.total,
hasNextPage: false
};
} else {
const publicData = limitData(data);
return publicData
}
} }
}) })
); );

View File

@@ -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: [

View File

@@ -0,0 +1,364 @@
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(),
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(mockLogRepository, 'findAndCount').mockReturnValue([logs, count, morePages]);
const {entities, total, hasNextPage} = await service.findLogsWithCount();
expect(entities).toEqual(logs);
expect(count).toEqual(total);
expect(hasNextPage).toEqual(morePages);
expect(mockLogRepository.findAndCount).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(mockLogRepository.findAndCount).toHaveBeenCalled();
})
it('update => should update a log entry', async () => {
const id: string = 'd685f1ca-28e0-40b9-8713-74467db12965';
const logDto = {
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
date: new Date('2026-02-21'),
aircraftMakeModel: 'Cessna 172M',
aircraftIdentity: 'N12345',
routeFrom: 'KMSP',
routeTo: 'KMSP',
durationOfFlight: 1,
singleEngineLand: 1,
simulatorAtd: null,
landingsDay: 1,
landingsNight: null,
groundTrainingReceived: null,
flightTrainingReceived: null,
crossCountry: 1,
night: null,
solo: 1,
pilotInCommand: 1,
instrumentActual: null,
instrumentSimulated: null,
instrumentApproaches: null,
instrumentHolds: null,
instrumentNavTrack: null,
notes: 'This is a test'
} as LogDto
const log = {
id: 'd685f1ca-28e0-40b9-8713-74467db12965',
pilotId: '7c4bae6b-9ec4-469e-8e16-fcbf0b940936',
date: new Date('2026-02-21'),
aircraftMakeModel: 'Cessna 172M',
aircraftIdentity: 'N12345',
routeFrom: 'KMSP',
routeTo: 'KMSP',
durationOfFlight: 1,
singleEngineLand: 1,
simulatorAtd: null,
landingsDay: 1,
landingsNight: null,
groundTrainingReceived: null,
flightTrainingReceived: null,
crossCountry: 1,
night: null,
solo: 1,
pilotInCommand: 1,
instrumentActual: null,
instrumentSimulated: null,
instrumentApproaches: null,
instrumentHolds: null,
instrumentNavTrack: null,
notes: 'This is a test',
tracks: []
} as LogEntity;
jest.spyOn(mockLogRepository, 'findOneBy').mockReturnValue(log);
jest.spyOn(mockLogRepository, 'save').mockReturnValue(logDto);
const result = await service.update(id, logDto);
expect(result).toEqual(logDto);
expect(mockLogRepository.findOneBy).toHaveBeenCalled();
expect(mockLogRepository.findOneBy).toHaveBeenCalledWith({ id: id });
expect(mockLogRepository.save).toHaveBeenCalled();
expect(mockLogRepository.save).toHaveBeenCalledWith(logDto);
})
})

View File

@@ -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,42 +18,52 @@ 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({
take,
skip,
relations: ['pilot', 'tracks'] relations: ['pilot', 'tracks']
});
}
async create(logDto: LogDto): Promise<InsertResult> {
try{
const pilotEntity: PilotEntity = await this.pilotService.find(logDto.pilotId);
if (pilotEntity) {
const { pilotId, ...newLogDto } = logDto;
const log = this.logRepository.create({
...newLogDto,
pilot: pilotEntity
}) })
return this.logRepository.insert(log); return {
} else { entities,
throw new CustomError('Pilot not found', 'Not found', 404); total,
} hasNextPage: skip + take < total
} catch (error) {
throw error
} }
} }
async update(id: string, log: LogDto): Promise<UpdateResult> { async findLogsWithTracks(skip?: number, take?: number): Promise<Logs> {
return await this.logRepository.update(id, log); const [entities, total] = await this.logRepository
.createQueryBuilder('logs')
.innerJoinAndSelect('logs.tracks', 'track')
.innerJoinAndSelect('logs.pilot', 'pilot')
.orderBy('date')
.getManyAndCount();
return {
entities,
total,
hasNextPage: skip + take < total
}
}
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> {

View File

@@ -0,0 +1,7 @@
import { LogEntity } from "src/log/log.entity";
export interface Logs {
entities: LogEntity[],
total: number,
hasNextPage: boolean
}

View File

@@ -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' })

View 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')
}
})
})

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 {

View 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 });
})
})

View File

@@ -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> {

View 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')
}
})
})

View File

@@ -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' })

View File

@@ -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: [

View 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)
})
})

View File

@@ -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> {

View File

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

View File

@@ -1,7 +1,7 @@
{ {
"name": "client", "name": "client",
"private": true, "private": true,
"version": "2.0.0-beta-2", "version": "2.0.2",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -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();
useEffect(() => { const getFlights = async (pageIndex: number, pageSize: number) => {
try {
const flights: LogbookEntry[] | undefined = logs?.filter((log: LogbookEntry) => { const response: AxiosResponse = await httpClient.get(`api/logs/flights`, {
if (log.tracks && log.tracks.length > 0) { params: {
return log; skip: pageIndex,
take: pageSize
} }
}) });
const flights: LogbookEntry[] = response.data.entities;
const hasMore: boolean = response.data.hasNextPage;
const total: number = response.data.total;
if (flights && flights.length > 0) { if (flights.length > 0) {
dispatch({ type: 'SET_FLIGHTS', payload: flights}) 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 }) dispatch({ type: 'SET_ALERT', payload: undefined })
}
} else { } else {
dispatch({ type: 'SET_ALERT', payload: { severity: 'info', message: 'No flights found' }}) dispatch({ type: 'SET_ALERT', payload: { severity: 'info', message: 'No flights found' }})
} }
}, [logs]) } 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(() => {
getFlights(state.pageIndex, state.pageSize);
}, [])
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> <div>
<LogbookCard logs={state.flights} mode='flights' /> <LogbookCard logs={state.flights} mode='flights' />
</div> </div>
{state.hasMoreFlights &&
<div className='flex items-center justify-center mb-5'>
<button className='btn btn-link' onClick={loadMore}>Load more</button>
</div>
} }
{logsLoading && [...Array(5)].map((_element, index) => { </div>
}
{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]' />

View File

@@ -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;
} }

View File

@@ -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': {

View File

@@ -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>
@@ -485,7 +490,9 @@ const LogForm = () => {
type='number' type='number'
className='input w-full' className='input w-full'
disabled={state.isDisabled} disabled={state.isDisabled}
onChange={onChange} min={0}
onChange={(event) => onChange(Number(event.target.value))}
step={0.1}
value={value} value={value}
/> />
)} )}
@@ -505,6 +512,7 @@ const LogForm = () => {
className='textarea w-full' className='textarea w-full'
disabled={state.isDisabled} disabled={state.isDisabled}
onChange={onChange} onChange={onChange}
value={value}
></textarea> ></textarea>
)} )}
/> />

View File

@@ -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;
} }

View File

@@ -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 = {

View File

@@ -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,
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.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,42 @@ 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])
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)}
@@ -495,9 +556,25 @@ const Logbook: React.FC<unknown> = () => {
</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={3}>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 +660,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'>

View File

@@ -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;
} }

View File

@@ -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;
} }

View File

@@ -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>

View File

@@ -1,3 +1,4 @@
import { useState } from "react";
import { LogbookDrawerProps } from "./LogbookDrawerProps.interface"; import { LogbookDrawerProps } from "./LogbookDrawerProps.interface";
import LogForm from "../logForm/LogForm"; import LogForm from "../logForm/LogForm";
import TracksForm from "../tracksForm/TracksForm"; import TracksForm from "../tracksForm/TracksForm";
@@ -9,7 +10,8 @@ import { FormMode } from "../../enums/formMode";
import httpClient from "../../httpClient/httpClient"; import httpClient from "../../httpClient/httpClient";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { useLogbookContext } from "../../hooks/logbookContext/UseLogbookContext"; import { useLogbookContext } from "../../hooks/logbookContext/UseLogbookContext";
import { ChangeEventHandler, MouseEventHandler, useState } from "react"; import { useBreakpoints } from "../../hooks/useBreakpoints/UseBreakpoints";
import { ScreenSize } from "../../enums/screenSize";
const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => { const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
const [activeTab, setActiveTab] = useState<string>('time'); const [activeTab, setActiveTab] = useState<string>('time');
@@ -40,6 +42,7 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
}; };
const methods = useForm(); const methods = useForm();
const logbookContext = useLogbookContext() const logbookContext = useLogbookContext()
const { screenSize } = useBreakpoints();
const onCancel = () => { const onCancel = () => {
methods.reset(defaultValues); methods.reset(defaultValues);
@@ -49,6 +52,7 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
const onSubmit = async (data: unknown) => { const onSubmit = async (data: unknown) => {
try { try {
logbookContext.dispatch({ type: 'SET_IS_FORM_DISABLED', payload: true });
logbookContext.dispatch({ type: 'SET_IS_FORM_LOADING', payload: true }); logbookContext.dispatch({ type: 'SET_IS_FORM_LOADING', payload: true });
if (!logbookContext.state.selectedLogId) { if (!logbookContext.state.selectedLogId) {
@@ -58,21 +62,20 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
} }
methods.reset(defaultValues); methods.reset(defaultValues);
logbookContext.dispatch({ type: 'SET_IS_FORM_DISABLED', payload: false });
logbookContext.dispatch({ type: 'SET_FORM_MODE', payload: FormMode.CANCEL });
} catch (error) { } catch (error) {
const axiosError = error as AxiosError; const axiosError = error as AxiosError;
logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: { severity: 'error', message: axiosError.message }}); logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: { severity: 'error', message: axiosError.message }});
} finally { } finally {
logbookContext.dispatch({ type: 'SET_IS_FORM_LOADING', payload: false }); logbookContext.dispatch({ type: 'SET_IS_FORM_LOADING', payload: false });
logbookContext.dispatch({ type: 'SET_IS_FORM_DISABLED', payload: false });
logbookContext.dispatch({ type: 'SET_FORM_MODE', payload: FormMode.CANCEL });
onOpenClose(FormMode.CANCEL);
} }
}; };
const onSelectedKeyChanged = (tab: string) => {
setActiveTab(tab)
}
const onTabClicked = (event: any) => { const onTabClicked = (event: any) => {
setActiveTab(event.target.ariaLabel) setActiveTab(event.target.ariaLabel)
} }
@@ -86,7 +89,7 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
aria-label='close-sidebar' aria-label='close-sidebar'
className='drawer-overlay' className='drawer-overlay'
></label> ></label>
<div className='menu bg-base-100 text-base-content min-h-full p-4' style={{ width: '25%' }}> <div className={`menu bg-base-100 text-base-content min-h-full p-4 ${screenSize === ScreenSize.SM ? 'w-full' : screenSize === ScreenSize.MD ? 'w-[66%]' : 'w-[25%]'}`}>
<FormProvider {...methods}> <FormProvider {...methods}>
<form className='prose max-w-none' onSubmit={methods.handleSubmit(onSubmit)} style={{ paddingBottom: '50px' }}> <form className='prose max-w-none' onSubmit={methods.handleSubmit(onSubmit)} style={{ paddingBottom: '50px' }}>
<div className='grid grid-cols-12 mb-6'> <div className='grid grid-cols-12 mb-6'>

View File

@@ -12,6 +12,8 @@ import httpClient from '../../httpClient/httpClient';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSave, faXmark } from '@fortawesome/free-solid-svg-icons'; import { faSave, faXmark } from '@fortawesome/free-solid-svg-icons';
import { states } from './states'; import { states } from './states';
import { useBreakpoints } from '../../hooks/useBreakpoints/UseBreakpoints';
import { ScreenSize } from '../../enums/screenSize';
const PilotForm: React.FC<IPilotFormProps> = ({ const PilotForm: React.FC<IPilotFormProps> = ({
pilotId, pilotId,
@@ -43,6 +45,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
}); });
const [isDisabled, setIsDisabled] = useState<boolean>(false); const [isDisabled, setIsDisabled] = useState<boolean>(false);
const [isError, setIsError] = useState<boolean>(false); const [isError, setIsError] = useState<boolean>(false);
const { screenSize } = useBreakpoints();
const onCancel = () => { const onCancel = () => {
methods.reset(defaultValues); methods.reset(defaultValues);
@@ -112,7 +115,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
aria-label='close-sidebar' aria-label='close-sidebar'
className='drawer-overlay' className='drawer-overlay'
></label> ></label>
<div className='menu bg-base-100 text-base-content min-h-full p-4' style={{ width: '25%' }}> <div className={`menu bg-base-100 text-base-content min-h-full p-4 ${screenSize === ScreenSize.SM ? 'w-full' : screenSize === ScreenSize.MD ? 'w-[66%]' : 'w-[25%]'}`}>
<FormProvider {...methods}> <FormProvider {...methods}>
<form className='prose max-w-none' onSubmit={methods.handleSubmit(onSubmit)} style={{ paddingBottom: '50px' }}> <form className='prose max-w-none' onSubmit={methods.handleSubmit(onSubmit)} style={{ paddingBottom: '50px' }}>
<div className='grid grid-cols-12 gap-3'> <div className='grid grid-cols-12 gap-3'>

Binary file not shown.

View File

@@ -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
View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@noahspan/flying", "name": "@noahspan/flying",
"version": "2.0.0-beta-2", "version": "2.0.2",
"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\"",