Skip to content

Commit

Permalink
Merge pull request #63 from ufabc-next/migrate/extension-routes
Browse files Browse the repository at this point in the history
Movendo rotas
  • Loading branch information
Joabesv authored Mar 22, 2024
2 parents f68ae71 + fd83d4c commit 2940bfa
Show file tree
Hide file tree
Showing 69 changed files with 1,772 additions and 1,102 deletions.
4 changes: 4 additions & 0 deletions apps/core/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import { loadPlugins } from './plugins.js';
import { internalRoutes, nextRoutes, publicRoutes } from './modules/routes.js';
import { nextUserRoutes } from './modules/NextUser/nextUser.module.js';
import { entitiesModule } from './modules/Entities/entities.module.js';

export async function buildApp(opts: FastifyServerOptions = {}) {
const app = fastify(opts);
Expand All @@ -18,6 +19,9 @@ export async function buildApp(opts: FastifyServerOptions = {}) {
await app.register(nextUserRoutes, {
prefix: '/v2',
});
await app.register(entitiesModule, {
prefix: '/v2',
});
await app.register(publicRoutes);
await app.register(nextRoutes, {
prefix: '/v2',
Expand Down
2 changes: 1 addition & 1 deletion apps/core/src/hooks/authenticate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const authenticate: onRequestAsyncHookHandler = async (request) => {
throw new Error('User does not exist or have been deactivated');
}
} catch (error) {
logger.error({ error }, 'error authenticating user');
logger.error(error, 'error authenticating user');
throw error;
}
};
File renamed without changes.
24 changes: 24 additions & 0 deletions apps/core/src/hooks/setStudentId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { currentQuad } from '@next/common';
import { StudentModel } from '@/models/Student.js';
import type { onRequestAsyncHookHandler } from 'fastify';

export const setStudentId: onRequestAsyncHookHandler = async (
request,
reply,
) => {
const { aluno_id } = request.query as { aluno_id: number };
const season = currentQuad();

const student = await StudentModel.findOne({
season,
aluno_id,
});

// if not found block route
if (!student) {
return reply.forbidden('Student_id');
}

// if not, pass
return;
};
1 change: 1 addition & 0 deletions apps/core/src/models/Comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,4 +164,5 @@ commentSchema.index({
});

export type Comment = InferSchemaType<typeof commentSchema>;
export type CommentDocument = ReturnType<(typeof CommentModel)['hydrate']>;
export const CommentModel = model('comments', commentSchema);
3 changes: 1 addition & 2 deletions apps/core/src/models/GraduationHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,13 @@ const POSSIBLE_SITUATIONS = [
const CATEGORIES = ['-', 'Opção Limitada', 'Obrigatória'] as const;
const PERIODO = ['1', '2', '3'] as const;

type GraduationHistoryCoefficients = Record<number, CoefficientsMap>;
type GraduationHistory = {
ra: number;
disciplinas: InferSchemaType<typeof GraduationHistoryDisciplinasSchema>[];
curso: string;
grade: string;
graduation: Types.ObjectId;
coefficients: GraduationHistoryCoefficients[];
coefficients: Record<number, CoefficientsMap>;
};

type GraduationHistoryModel = Model<GraduationHistory, {}>;
Expand Down
1 change: 1 addition & 0 deletions apps/core/src/models/Student.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,5 @@ studentSchema.pre('findOneAndUpdate', function () {
});

export type Student = InferSchemaType<typeof studentSchema>;
export type StudentDocument = ReturnType<(typeof StudentModel)['hydrate']>;
export const StudentModel = model('alunos', studentSchema);
1 change: 1 addition & 0 deletions apps/core/src/models/Teacher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ teacherSchema.pre('save', function () {
});

export type Teacher = InferSchemaType<typeof teacherSchema>;
export type TeacherDocument = ReturnType<(typeof TeacherModel)['hydrate']>;
export const TeacherModel = model('teachers', teacherSchema);
193 changes: 193 additions & 0 deletions apps/core/src/modules/Entities/disciplinas/disciplina.handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { sortBy as LodashSortBy } from 'lodash-es';
import { courseId, currentQuad } from '@next/common';
import { StudentModel } from '@/models/Student.js';
import type { Disciplina } from '@/models/Disciplina.js';
import type { DisciplinaService } from './disciplina.service.js';
import type { FastifyReply, FastifyRequest } from 'fastify';

export type DisciplinaKicksRequest = {
Params: {
disciplinaId: number;
};
Querystring: {
aluno_id: number;
sort?: string;
};
};

export class DisciplinaHandler {
constructor(private readonly disciplinaService: DisciplinaService) {}

async listDisciplinas(request: FastifyRequest) {
const season = currentQuad();
const { redis } = request.server;
const cacheKey = `all_disciplinas_${season}`;
const cachedResponse = await redis.get(cacheKey);

if (cachedResponse) {
return cachedResponse;
}

const disciplinas = await this.disciplinaService.findDisciplinas(season);

await redis.set(cacheKey, JSON.stringify(disciplinas), 'EX', 60 * 60 * 24);
return disciplinas;
}

async listDisciplinasKicks(
request: FastifyRequest<DisciplinaKicksRequest>,
reply: FastifyReply,
) {
const { disciplinaId } = request.params;
const { sort } = request.query;

if (!disciplinaId) {
await reply.badRequest('Missing DisciplinaId');
}

const season = currentQuad();
const disciplina = await this.disciplinaService.findDisciplina(
season,
disciplinaId,
);

if (!disciplina) {
return reply.notFound('Disciplina not found');
}

// create sort mechanism
const kicks = sort || kickRule(disciplina);
// @ts-expect-error for now
const order = [kicks.length || 0].fill('desc');

// turno must have a special treatment
const turnoIndex = kicks.indexOf('turno');
if (turnoIndex !== -1) {
// @ts-expect-error for now
order[turnoIndex] = disciplina.turno === 'diurno' ? 'asc' : 'desc';
}

const isAfterKick = [disciplina.after_kick].filter(Boolean).length;
const resolveKicked = resolveMatricula(disciplina, isAfterKick);

const kickedsMap = new Map(
resolveKicked.map((kicked) => [kicked.aluno_id, kicked]),
);
const students = await this.disciplinaService.findStudentCourses(
season,
resolveKicked.map((kick) => kick.aluno_id),
);

const interIds = [
await courseId(
'Bacharelado em Ciência e Tecnologia',
season,
// TODO(Joabe): refac later
StudentModel,
),
await courseId(
'Bacharelado em Ciências e Humanidades',
season,
// TODO(Joabe): refac later
StudentModel,
),
];

const obrigatorias = getObrigatoriasFromDisciplinas(
disciplina.obrigatorias,
...interIds,
);

const studentsWithGraduationInfo = students.map((student) => {
const reserva = obrigatorias.includes(student.cursos.id_curso);
const graduationToStudent = Object.assign(
{
aluno_id: student.aluno_id,
cr: '-',
cp: student.cursos.cp,
ik: reserva ? student.cursos.ind_afinidade : 0,
reserva,
turno: student.cursos.turno,
curso: student.cursos.nome_curso,
},
kickedsMap.get(student.aluno_id) || {},
);

return graduationToStudent;
});

const sortedStudents = LodashSortBy(studentsWithGraduationInfo, kicks);

const uniqueStudents = [];
const uniqueStudentIds = new Set();

for (const student of sortedStudents) {
if (!uniqueStudentIds.has(student.aluno_id)) {
uniqueStudents.push(student);
uniqueStudentIds.add(student.aluno_id);
}
}

return uniqueStudents;
}
}

function kickRule(disciplina: Disciplina) {
const season = currentQuad();
let coeffRule = null;
if (
season === '2020:2' ||
season === '2020:3' ||
season === '2021:1' ||
season === '2021:2' ||
season === '2021:3' ||
season === '2022:1' ||
season === '2022:2' ||
season === '2022:3' ||
season === '2023:1' ||
season === '2023:2' ||
season === '2023:3' ||
season === '2024:1' ||
season === '2024:2' ||
season === '2024:3' ||
season === '2025:1'
) {
coeffRule = ['cp', 'cr'];
} else {
coeffRule = disciplina.ideal_quad ? ['cr', 'cp'] : ['cp', 'cr'];
}

return ['reserva', 'turno', 'ik'].concat(coeffRule);
}

function resolveMatricula(disciplina: Disciplina, isAfterKick: number) {
// if kick has not arrived, not one has been kicked
if (!isAfterKick) {
const registeredStudents = disciplina.alunos_matriculados || [];
return registeredStudents.map((student) => ({
aluno_id: student,
}));
}

// check diff between before_kick and after_kick
const kicked = differenceOnKicks(
disciplina.before_kick ?? [],
disciplina.after_kick ?? [],
);
// return who has been kicked
return disciplina.before_kick.map((student) => ({
aluno_id: student,
kicked: kicked.includes(student),
}));
}

function getObrigatoriasFromDisciplinas(
obrigatorias: number[],
...filterList: Awaited<ReturnType<typeof courseId>>[]
) {
const removeSet = new Set(filterList);
return obrigatorias.filter((obrigatoria) => !removeSet.has(obrigatoria));
}

const differenceOnKicks = (beforeKick: number[], afterKick: number[]) =>
beforeKick.filter((kick) => !afterKick.includes(kick));
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { Disciplina, DisciplinaModel } from '@/models/Disciplina.js';
import type { Student, StudentModel } from '@/models/Student.js';
import type { FilterQuery, PipelineStage, ProjectionType } from 'mongoose';

type StudentAggregate = Student & {
cursos: Student['cursos'][number];
};

interface EntitiesDisciplinaRepository {
findMany(
filter: FilterQuery<Disciplina>,
mapping?: ProjectionType<Disciplina>,
populateFields?: string[],
): Promise<Disciplina[] | null>;
findOne(filter: FilterQuery<Disciplina>): Promise<Disciplina | null>;
findCursos(filter: FilterQuery<Student>): Promise<StudentAggregate[]>;
}

export class DisciplinaRepository implements EntitiesDisciplinaRepository {
constructor(
private readonly disciplinaService: typeof DisciplinaModel,
private readonly studentService: typeof StudentModel,
) {}

async findMany(
filter: FilterQuery<Disciplina>,
mapping?: ProjectionType<Disciplina>,
populateFields?: string[],
) {
if (populateFields) {
const disciplinas = await this.disciplinaService
// eslint-disable-next-line unicorn/no-array-callback-reference, unicorn/no-array-method-this-argument
.find(filter, mapping)
.populate(populateFields)
.lean<Disciplina[]>({ virtuals: true });
return disciplinas;
}
const disciplinas = await this.disciplinaService
// eslint-disable-next-line unicorn/no-array-callback-reference, unicorn/no-array-method-this-argument
.find(filter, mapping)
.lean<Disciplina[]>({ virtuals: true });
return disciplinas;
}

async findOne(filter: FilterQuery<Disciplina>) {
const disciplina = await this.disciplinaService.findOne(filter);

return disciplina;
}

async findCursos(pipeline: PipelineStage[]) {
const students =
await this.studentService.aggregate<StudentAggregate>(pipeline);

return students;
}
}
29 changes: 29 additions & 0 deletions apps/core/src/modules/Entities/disciplinas/disciplina.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { DisciplinaModel } from '@/models/Disciplina.js';
import { StudentModel } from '@/models/Student.js';
import { setStudentId } from '@/hooks/setStudentId.js';
import { DisciplinaRepository } from './disciplina.repository.js';
import {
DisciplinaHandler,
type DisciplinaKicksRequest,
} from './disciplina.handlers.js';
import { DisciplinaService } from './disciplina.service.js';

import type { FastifyInstance } from 'fastify';

// eslint-disable-next-line require-await
export async function disciplinasRoute(app: FastifyInstance) {
const disciplinaRepository = new DisciplinaRepository(
DisciplinaModel,
StudentModel,
);
const disciplinaService = new DisciplinaService(disciplinaRepository);
app.decorate('disciplinaService', disciplinaService);
const disciplinaHandler = new DisciplinaHandler(disciplinaService);

app.get('/disciplina', disciplinaHandler.listDisciplinas);
app.get<DisciplinaKicksRequest>(
'/disciplina/:disciplinaId/kicks',
{ onRequest: [setStudentId] },
disciplinaHandler.listDisciplinasKicks,
);
}
Loading

0 comments on commit 2940bfa

Please sign in to comment.