Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import AppDataSource from './data-source';
import { ApplicationsController } from './applications/applications.controller';
import { ApplicationsService } from './applications/applications.service';
import { Application } from './applications/application.entity';

@Module({
imports: [TypeOrmModule.forRoot(AppDataSource.options)],
controllers: [AppController],
providers: [AppService],
imports: [
TypeOrmModule.forRoot(AppDataSource.options),
TypeOrmModule.forFeature([Application]),
],
controllers: [AppController, ApplicationsController],
providers: [AppService, ApplicationsService],
})
export class AppModule {}
63 changes: 31 additions & 32 deletions apps/backend/src/applications/application.entity.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,48 @@
import { Entity, Column } from 'typeorm';
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

import type { AppStatus, ExperienceType, Interest, School } from './types';
import { AppStatus, ExperienceType, InterestArea, School } from './types';

@Entity()
@Entity('application')
export class Application {
@Column({ primary: true })
appId: number;
@PrimaryGeneratedColumn()
appId!: number;

@Column()
appStatus: AppStatus;
@Column({ type: 'enum', enum: AppStatus, default: AppStatus.APP_SUBMITTED })
appStatus!: AppStatus;

@Column()
daysAvailable: string;
@Column({ type: 'varchar' })
daysAvailable!: string;

@Column()
experienceType: ExperienceType;
@Column({ type: 'enum', enum: ExperienceType })
experienceType!: ExperienceType;

// Array of S3 file URLs
@Column()
fileUploads: string[];
@Column('text', { array: true, default: [] })
fileUploads!: string[];

@Column()
interest: Interest;
@Column({ type: 'enum', enum: InterestArea })
interest!: InterestArea;

@Column()
license: string;
@Column({ type: 'varchar' })
license!: string;

@Column()
isInternational: boolean;
@Column({ type: 'boolean', default: false })
isInternational!: boolean;

@Column()
isLearner: boolean;
@Column({ type: 'boolean', default: false })
isLearner!: boolean;

@Column()
phone: string;
@Column({ type: 'varchar' })
phone!: string;

@Column()
school: School;
@Column({ type: 'enum', enum: School })
school!: School;

@Column()
referred: boolean;
@Column({ type: 'boolean', default: false, nullable: true })
referred?: boolean;

@Column()
referredEmail: string;
@Column({ type: 'varchar', nullable: true })
referredEmail?: string;

@Column()
weeklyHours: number;
@Column({ type: 'int' })
weeklyHours!: number;
}
149 changes: 149 additions & 0 deletions apps/backend/src/applications/application.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { NotFoundException } from '@nestjs/common';
import { ApplicationsService } from './applications.service';
import { Application } from './application.entity';
import { CreateApplicationDto } from './dto/create-application.request.dto';
import { AppStatus, ExperienceType, InterestArea, School } from './types';

describe('ApplicationsService', () => {
let service: ApplicationsService;
let repository: Repository<Application>;

const mockRepository = {
find: jest.fn(),
findOne: jest.fn(),
save: jest.fn(),
create: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ApplicationsService,
{
provide: getRepositoryToken(Application),
useValue: mockRepository,
},
],
}).compile();

service = module.get<ApplicationsService>(ApplicationsService);
repository = module.get<Repository<Application>>(
getRepositoryToken(Application),
);
});

afterEach(() => {
jest.clearAllMocks();
});

it('should be defined', () => {
expect(service).toBeDefined();
});

describe('findAll', () => {
it('should return an array of applications', async () => {
const mockApplications: Application[] = [
{
appId: 1,
appStatus: AppStatus.APP_SUBMITTED,
daysAvailable: 'Monday, Tuesday',
experienceType: ExperienceType.BS,
fileUploads: [],
interest: InterestArea.NURSING,
license: null,
isInternational: false,
isLearner: false,
phone: '123-456-7890',
school: School.HARVARD_MEDICAL_SCHOOL,
referred: false,
referredEmail: null,
weeklyHours: 20,
},
];

mockRepository.find.mockResolvedValue(mockApplications);

const result = await service.findAll();

expect(repository.find).toHaveBeenCalled();
expect(result).toEqual(mockApplications);
});
});

describe('findById', () => {
it('should return a single application', async () => {
const mockApplication: Application = {
appId: 1,
appStatus: AppStatus.APP_SUBMITTED,
daysAvailable: 'Monday, Tuesday',
experienceType: ExperienceType.BS,
fileUploads: [],
interest: InterestArea.NURSING,
license: null,
isInternational: false,
isLearner: false,
phone: '123-456-7890',
school: School.HARVARD_MEDICAL_SCHOOL,
referred: false,
referredEmail: null,
weeklyHours: 20,
};

mockRepository.findOne.mockResolvedValue(mockApplication);

const result = await service.findById(1);

expect(repository.findOne).toHaveBeenCalledWith({ where: { appId: 1 } });
expect(result).toEqual(mockApplication);
});

it('should throw NotFoundException when application is not found', async () => {
const nonExistentId = 999;

mockRepository.findOne.mockResolvedValue(null);

await expect(service.findById(nonExistentId)).rejects.toThrow(
new NotFoundException(`Application with ID ${nonExistentId} not found`),
);

expect(repository.findOne).toHaveBeenCalledWith({
where: { appId: nonExistentId },
});
});
});

describe('create', () => {
it('should create and save a new application', async () => {
const createApplicationDto: CreateApplicationDto = {
appStatus: AppStatus.APP_SUBMITTED,
daysAvailable: 'Monday, Tuesday',
experienceType: ExperienceType.BS,
fileUploads: [],
interest: InterestArea.NURSING,
license: null,
isInternational: false,
isLearner: false,
phone: '123-456-7890',
school: School.HARVARD_MEDICAL_SCHOOL,
referred: false,
referredEmail: null,
weeklyHours: 20,
};

const savedApplication: Application = {
appId: 1,
...createApplicationDto,
};

mockRepository.save.mockResolvedValue(savedApplication);

const result = await service.create(createApplicationDto);

expect(repository.save).toHaveBeenCalled();
expect(result).toEqual(savedApplication);
});
});
});
40 changes: 40 additions & 0 deletions apps/backend/src/applications/applications.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {
Body,
Controller,
Get,
Param,
ParseIntPipe,
Post,
Request,
} from '@nestjs/common';
import { ApplicationsService } from './applications.service';
import { Application } from './application.entity';
import { CreateApplicationDto } from './dto/create-application.request.dto';
import { ApiTags } from '@nestjs/swagger';

@ApiTags('Applications')
@Controller('applications')
export class ApplicationsController {
constructor(private applicationsService: ApplicationsService) {}

@Get()
async getAllApplications(@Request() req): Promise<Application[]> {
return await this.applicationsService.findAll();
}

@Get('/:appId')
async getApplicationById(
@Param('appId', ParseIntPipe) appId: number,
@Request() req,
): Promise<Application> {
return await this.applicationsService.findById(appId);
}

@Post()
async createApplication(
@Body() createApplicationDto: CreateApplicationDto,
@Request() req,
): Promise<Application> {
return await this.applicationsService.create(createApplicationDto);
}
}
12 changes: 12 additions & 0 deletions apps/backend/src/applications/applications.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationsController } from './applications.controller';
import { ApplicationsService } from './applications.service';
import { Application } from './application.entity';

@Module({
imports: [TypeOrmModule.forFeature([Application])],
controllers: [ApplicationsController],
providers: [ApplicationsService],
})
export class ApplicationsModule {}
36 changes: 36 additions & 0 deletions apps/backend/src/applications/applications.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Application } from './application.entity';
import { CreateApplicationDto } from './dto/create-application.request.dto';

@Injectable()
export class ApplicationsService {
constructor(
@InjectRepository(Application)
private applicationRepository: Repository<Application>,
) {}

async findAll(): Promise<Application[]> {
return await this.applicationRepository.find();
}

async findById(appId: number): Promise<Application> {
const application: Application = await this.applicationRepository.findOne({
where: { appId },
});

if (!application) {
throw new NotFoundException(`Application with ID ${appId} not found`);
}

return application;
}

async create(
createApplicationDto: CreateApplicationDto,
): Promise<Application> {
const application = this.applicationRepository.create(createApplicationDto);
return await this.applicationRepository.save(application);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {
IsBoolean,
IsEnum,
IsNumber,
IsString,
IsArray,
IsOptional,
} from 'class-validator';
import { AppStatus, ExperienceType, InterestArea, School } from '../types';

export class CreateApplicationDto {
@IsEnum(AppStatus)
appStatus: AppStatus;

@IsString()
daysAvailable: string;

@IsEnum(ExperienceType)
experienceType: ExperienceType;

@IsArray()
@IsString({ each: true })
fileUploads: string[];

@IsEnum(InterestArea)
interest: InterestArea;

@IsString()
license: string;

@IsBoolean()
isInternational: boolean;

@IsBoolean()
isLearner: boolean;

@IsString()
phone: string;

@IsEnum(School)
school: School;

@IsBoolean()
@IsOptional()
referred?: boolean;

@IsString()
@IsOptional()
referredEmail?: string;

@IsNumber()
weeklyHours: number;
}
Loading