Skip to content
Snippets Groups Projects
Unverified Commit b64f7ddf authored by Ivan Alglave's avatar Ivan Alglave Committed by GitHub
Browse files

Merge pull request #5 from ivanalglave/crud-internship

Crud internship
parents 54e9b981 bf849b1e
No related branches found
No related tags found
No related merge requests found
Showing
with 18136 additions and 13 deletions
...@@ -103,8 +103,5 @@ dist ...@@ -103,8 +103,5 @@ dist
# TernJS port file # TernJS port file
.tern-port .tern-port
# package-lock.json file
package-lock.json
# config files # config files
config.json config.json
This diff is collapsed.
...@@ -13,10 +13,10 @@ ...@@ -13,10 +13,10 @@
"prebuild": "rimraf dist", "prebuild": "rimraf dist",
"build": "nest build", "build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start", "start": "cross-env NODE_ENV=dev nest start",
"start:dev": "nest start --watch", "start:dev": "cross-env NODE_ENV=dev nest start --watch",
"start:debug": "nest start --debug --watch", "start:debug": "cross-env NODE_ENV=dev nest start --debug --watch",
"start:prod": "node dist/main", "start:prod": "cross-env NODE_ENV=prod node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest", "test": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch",
...@@ -47,6 +47,7 @@ ...@@ -47,6 +47,7 @@
"@types/supertest": "^2.0.11", "@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0", "@typescript-eslint/parser": "^5.0.0",
"cross-env": "^7.0.3",
"eslint": "^8.0.1", "eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
......
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose'; import { MongooseModule } from '@nestjs/mongoose';
import { mongodb } from './config'; import config from './config';
import { PeopleModule } from './people/people.module'; import { PeopleModule } from './people/people.module';
import { GroupsModule } from './groups/groups.module'; import { GroupsModule } from './groups/groups.module';
import { InternshipsModule } from './internships/internships.module';
@Module({ @Module({
imports: [PeopleModule, GroupsModule, MongooseModule.forRoot(mongodb.uri)], imports: [
PeopleModule,
GroupsModule,
InternshipsModule,
MongooseModule.forRoot(config.mongodb.uri),
],
}) })
export class AppModule {} export class AppModule {}
{
"server": {
"uri": "localhost",
"port": 3001
},
"mongodb": {
"uri": "mongodb://localhost:27017/internship-manager"
}
}
import * as _config from './config.json';
import { IConfig } from './config.model'; import { IConfig } from './config.model';
import { readFileSync } from 'fs';
import * as path from 'path';
import { exit } from 'process';
const config = _config as IConfig; // Store file local path in var for lisibility
const CONFIG_DEV = 'config.json';
const CONFIG_PROD = 'config.prod.json';
export const server = config.server; let config;
export const mongodb = config.mongodb; // Load config based on env
switch (process.env.NODE_ENV) {
case 'dev':
// Load 'dev' config
config = JSON.parse(
readFileSync(path.join(__dirname, CONFIG_DEV), 'utf-8'),
) as IConfig;
break;
case 'prod':
// Load 'prod' config
config = JSON.parse(
readFileSync(path.join(__dirname, CONFIG_PROD), 'utf-8'),
) as IConfig;
break;
default:
// This happens when the environment isn't set
console.log(
'\x1b[31mFATAL: Cannot load config.\nInvalid application environment. Did you read the \x1b[4mREADME\x1b[0m\x1b[31m ?\x1b[0m',
);
exit(-1);
}
// Export config
export default config; export default config;
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { BAD_REQUEST, CONFLICT } from 'src/shared/HttpError';
import {
isStateValid,
STATE_COMPANY_SIGNS_INTERNSHIP_AGREEMENT,
STATE_DEAN_SIGNS_INTERNSHIP_AGREEMENT,
STATE_RESPONSIBLE_ACCEPTS_INTERNSHIP_INFORMATION,
STATE_RESPONSIBLE_SIGNS_INTERNSHIP_AGREEMENT,
STATE_SECRETARY_ESTABLISHES_INTERNSHIP_AGREEMENT,
STATE_STUDENT_ENTERS_INTERNSHIP_INFORMATION,
STATE_STUDENT_SIGNS_INTERNSHIP_AGREEMENT,
} from 'src/shared/InternshipState';
import { CreateInternshipDto } from '../dto/create-internship.dto';
import { InternshipDto } from '../dto/internship.dto';
import { Internship } from '../schemas/internship.schema';
@Injectable()
export class InternshipDao {
constructor(
@InjectModel(Internship.name)
private readonly _internshipModel: Model<Internship>,
) {}
find = (): Promise<Internship[]> =>
new Promise((resolve, reject) => {
this._internshipModel.find({}, {}, {}, (err, value) => {
if (err) reject(err.message);
if (!value) reject('No values');
resolve(value);
});
});
findByStudentId = (studentId: string): Promise<Internship | void> =>
new Promise((resolve, reject) => {
this._internshipModel.findOne({ studentId }, {}, {}, (err, value) => {
if (err) reject(err.message);
if (!value) reject(new NotFoundException());
resolve(value);
});
});
save = (internship: CreateInternshipDto): Promise<Internship> =>
new Promise((resolve, reject) => {
// Use updateOne with `upsert: true` to only insert when no other document has the same studentId to prevent duplicata
const decoratedInternship = this.toInternshipDtoWithTracking(internship);
this._internshipModel.updateOne(
{ studentId: internship.studentId },
{ $setOnInsert: decoratedInternship },
{
upsert: true,
runValidators: true,
},
(err, value) => {
const { upsertedCount } = value;
if (err) reject(err.message);
if (upsertedCount === 0) reject(CONFLICT);
resolve(decoratedInternship as Internship);
},
);
});
findByStudentIdAndUpdate = (
studentId: string,
internship: CreateInternshipDto,
): Promise<Internship | void> =>
new Promise((resolve, reject) => {
// Check if information modification is allowed -> current state is information input by student and updating is allowed
if (studentId !== internship.studentId) reject(BAD_REQUEST);
const decoratedInternship = this.toInternshipDtoWithTracking(internship);
this._internshipModel.findOneAndReplace(
{
studentId,
'tracking.state': STATE_STUDENT_ENTERS_INTERNSHIP_INFORMATION,
},
decoratedInternship,
{
new: true,
runValidators: true,
},
(err, value) => {
if (err) reject(err.message);
// if (typeof value !== typeof Internship) reject(INTERNAL);
resolve(value as Internship);
},
);
});
private updateTrackingState = (
studentId: string,
state: string,
nextState: string,
contentHolder: string,
content: string | boolean,
callback: (err: any, value: any) => void,
) => {
this._internshipModel.findOneAndUpdate(
{
studentId,
'tracking.state': state,
},
{
$set: {
'tracking.state': nextState,
[contentHolder]: content,
},
},
{
new: true,
runValidators: true,
},
callback,
);
};
findByStudentIdAndUpdateTracking = (
studentId: string,
state: string,
content: string | boolean,
): Promise<Internship | void> =>
new Promise((resolve, reject) => {
if (!isStateValid(state)) reject(BAD_REQUEST);
console.log(typeof content);
let nextState: string, contentHolder: string;
let valid = false;
switch (state) {
case STATE_STUDENT_ENTERS_INTERNSHIP_INFORMATION: {
if (typeof content === 'boolean' && content === true) {
nextState = STATE_RESPONSIBLE_ACCEPTS_INTERNSHIP_INFORMATION;
contentHolder = 'tracking.studentEntersInternshipInformation';
valid = true;
} else reject(BAD_REQUEST);
break;
}
case STATE_RESPONSIBLE_ACCEPTS_INTERNSHIP_INFORMATION: {
if (typeof content === 'boolean' && content === true) {
// content === true -> Responsible agrees with internship
nextState = STATE_SECRETARY_ESTABLISHES_INTERNSHIP_AGREEMENT;
contentHolder = 'tracking.responsibleAcceptsInternshipInformation';
valid = true;
} else if (typeof content === 'boolean' && content === false) {
// content === false -> Responsible did not agree with internship, go back to student entering information
nextState = STATE_STUDENT_ENTERS_INTERNSHIP_INFORMATION;
contentHolder = 'tracking.responsibleAcceptsInternshipInformation';
valid = true;
} else reject(BAD_REQUEST);
break;
}
case STATE_SECRETARY_ESTABLISHES_INTERNSHIP_AGREEMENT: {
if (typeof content !== 'string') reject(BAD_REQUEST);
else {
nextState = STATE_STUDENT_SIGNS_INTERNSHIP_AGREEMENT;
contentHolder = 'tracking.secretaryEstablishesInternshipAgreement';
valid = true;
}
break;
}
case STATE_STUDENT_SIGNS_INTERNSHIP_AGREEMENT: {
if (typeof content !== 'string') reject(BAD_REQUEST);
else {
nextState = STATE_RESPONSIBLE_SIGNS_INTERNSHIP_AGREEMENT;
contentHolder = 'tracking.studentSignsInternshipAgreement';
valid = true;
}
break;
}
case STATE_RESPONSIBLE_SIGNS_INTERNSHIP_AGREEMENT: {
if (typeof content !== 'string') reject(BAD_REQUEST);
else {
nextState = STATE_COMPANY_SIGNS_INTERNSHIP_AGREEMENT;
contentHolder = 'tracking.responsibleSignsInternshipAgreement';
valid = true;
}
break;
}
case STATE_COMPANY_SIGNS_INTERNSHIP_AGREEMENT: {
if (typeof content !== 'string') reject(BAD_REQUEST);
else {
nextState = STATE_DEAN_SIGNS_INTERNSHIP_AGREEMENT;
contentHolder = 'tracking.companySignsInternshipAgreement';
valid = true;
}
break;
}
case STATE_DEAN_SIGNS_INTERNSHIP_AGREEMENT: {
if (typeof content !== 'string') reject(BAD_REQUEST);
else {
nextState = '/';
contentHolder = 'tracking.deanSignsInternshipAgreement';
valid = true;
}
break;
}
}
if (valid) {
this.updateTrackingState(
studentId,
state,
nextState,
contentHolder,
content,
(err, value) => {
if (err) reject(err);
else if (!value) reject(BAD_REQUEST);
else resolve(value);
},
);
}
});
findByStudentIdAndRemove = (studentId: string): Promise<Internship | void> =>
new Promise((resolve, reject) => {
this._internshipModel.findOneAndDelete({ studentId }, {}, (err) => {
if (err) reject(err.message);
resolve();
});
});
toInternshipDtoWithTracking = (
createInternshipDto: CreateInternshipDto,
): InternshipDto => {
return {
...createInternshipDto,
tracking: {
state: STATE_STUDENT_ENTERS_INTERNSHIP_INFORMATION,
},
};
};
}
import { Type } from 'class-transformer';
import {
IsString,
IsNotEmpty,
IsDefined,
IsNotEmptyObject,
ValidateNested,
} from 'class-validator';
import { InformationDto } from './nested-create/information.dto';
export class CreateInternshipDto {
@IsString()
@IsNotEmpty()
studentId: string;
@IsDefined()
@IsNotEmptyObject()
@ValidateNested()
@Type(() => InformationDto)
information: InformationDto;
}
import { Type } from 'class-transformer';
import {
IsString,
IsNotEmpty,
IsDefined,
IsNotEmptyObject,
ValidateNested,
} from 'class-validator';
import { InformationDto } from './nested-create/information.dto';
import { TrackingDto } from './nested-create/tracking.dto';
export class InternshipDto {
@IsString()
@IsNotEmpty()
studentId: string;
@IsDefined()
@IsNotEmptyObject()
@ValidateNested()
@Type(() => InformationDto)
information: InformationDto;
@IsDefined()
@IsNotEmptyObject()
@ValidateNested()
@Type(() => TrackingDto)
tracking: TrackingDto;
}
import { IsString, IsNotEmpty } from 'class-validator';
export class AddressDto {
@IsString()
@IsNotEmpty()
street: string;
@IsString()
@IsNotEmpty()
city: string;
@IsString()
@IsNotEmpty()
postalCode: string;
@IsString()
@IsNotEmpty()
country: string;
}
import { Type } from 'class-transformer';
import {
IsString,
IsNotEmpty,
IsDateString,
IsDefined,
IsNotEmptyObject,
ValidateNested,
} from 'class-validator';
import { AddressDto } from './address.dto';
export class AffectationDto {
@IsString()
@IsNotEmpty()
service: string;
@IsString()
@IsNotEmpty()
responsibleName: string;
@IsString()
@IsNotEmpty()
responsibleEmail: string;
@IsString()
@IsNotEmpty()
responsiblePhone: string;
@IsString()
@IsNotEmpty()
responsibleFunction: string;
@IsDateString()
@IsNotEmpty()
dateStart: Date;
@IsDateString()
@IsNotEmpty()
dateEnd: Date;
@IsDefined()
@IsNotEmptyObject()
@ValidateNested()
@Type(() => AddressDto)
address: AddressDto;
}
import { Type } from 'class-transformer';
import {
IsString,
IsNotEmpty,
IsDefined,
IsNotEmptyObject,
ValidateNested,
} from 'class-validator';
import { AddressDto } from './address.dto';
export class CompanyDto {
@IsString()
@IsNotEmpty()
ceoName: string;
@IsString()
@IsNotEmpty()
companyName: string;
@IsString()
@IsNotEmpty()
hrContactName: string;
@IsString()
@IsNotEmpty()
hrContactEmail: string;
@IsDefined()
@IsNotEmptyObject()
@ValidateNested()
@Type(() => AddressDto)
address: AddressDto;
}
import { IsString, IsNotEmpty } from 'class-validator';
export class CompensationDto {
@IsString()
@IsNotEmpty()
gratificationAmount: string;
@IsString()
@IsNotEmpty()
modalities: string;
@IsString()
@IsNotEmpty()
othersAdvantages: string;
}
import { Type } from 'class-transformer';
import {
IsString,
IsNotEmpty,
IsDefined,
IsNotEmptyObject,
ValidateNested,
} from 'class-validator';
import { AffectationDto } from './affectation.dto';
import { CompanyDto } from './company.dto';
import { CompensationDto } from './compensation.dto';
import { StudentDto } from './student.dto';
export class InformationDto {
@IsDefined()
@IsNotEmptyObject()
@ValidateNested()
@Type(() => StudentDto)
student: StudentDto;
@IsDefined()
@IsNotEmptyObject()
@ValidateNested()
@Type(() => CompanyDto)
company: CompanyDto;
@IsDefined()
@IsNotEmptyObject()
@ValidateNested()
@Type(() => AffectationDto)
affectation: AffectationDto;
@IsDefined()
@IsNotEmptyObject()
@ValidateNested()
@Type(() => CompensationDto)
compensation: CompensationDto;
@IsString()
@IsNotEmpty()
internshipDescription: string;
}
import { Type } from 'class-transformer';
import {
IsString,
IsNotEmpty,
IsDateString,
IsDefined,
IsNotEmptyObject,
ValidateNested,
IsEmail,
IsAlphanumeric,
IsPhoneNumber,
} from 'class-validator';
import { AddressDto } from './address.dto';
export class StudentDto {
@IsString()
@IsNotEmpty()
completeName: string;
@IsString()
@IsPhoneNumber()
@IsNotEmpty()
phone: string;
@IsDateString()
@IsNotEmpty()
birthDate: Date;
@IsDefined()
@IsNotEmptyObject()
@ValidateNested()
@Type(() => AddressDto)
address: AddressDto;
@IsString()
@IsNotEmpty()
FormationAndSpecialty: string;
@IsString()
@IsEmail()
@IsNotEmpty()
email: string;
@IsString()
@IsAlphanumeric()
@IsNotEmpty()
socialSecurityNumber: string;
}
import { IsString, IsNotEmpty, IsBoolean, IsOptional } from 'class-validator';
export class TrackingDto {
@IsString()
@IsNotEmpty()
state: string;
@IsBoolean()
@IsOptional()
studentEntersInternshipInformation?: boolean;
@IsBoolean()
@IsOptional()
responsibleAcceptsInternshipInformation?: boolean;
@IsString()
@IsOptional()
secretaryEstablishesInternshipAgreement?: string;
@IsString()
@IsOptional()
studentSignsInternshipAgreement?: string;
@IsString()
@IsOptional()
responsibleSignsInternshipAgreement?: string;
@IsString()
@IsOptional()
companySignsInternshipAgreement?: string;
@IsString()
@IsOptional()
deanSignsInternshipAgreement?: string;
}
import { InformationEntity } from './nested-entities/information.entity';
import { TrackingEntity } from './nested-entities/tracking.entity';
export class InternshipEntity {
studentId: string;
information: InformationEntity;
tracking: TrackingEntity;
constructor(partial: Partial<InternshipEntity>) {
Object.assign(this, partial);
}
}
export class AddressEntity {
street: string;
postalCode: string;
city: string;
country: string;
constructor(partial: Partial<AddressEntity>) {
Object.assign(this, partial);
}
}
import { AddressEntity } from './address.entity';
export class AffectationEntity {
service: string;
responsibleName: string;
responsibleEmail: string;
responsiblePhone: string;
responsibleFunction: string;
dateStart: Date;
dateEnd: Date;
address: AddressEntity;
constructor(partial: Partial<AffectationEntity>) {
Object.assign(this, partial);
}
}
import { AddressEntity } from './address.entity';
export class CompanyEntity {
ceoName: string;
companyName: string;
hrContactName: string;
hrContactEmail: string;
address: AddressEntity;
constructor(partial: Partial<CompanyEntity>) {
Object.assign(this, partial);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment