Skip to content

Commit

Permalink
API Paging (#50)
Browse files Browse the repository at this point in the history
* Pagination of all routes
* Finally separating querification from validation
- Solves #33 & #50 
- More simple tests for helpers.ts
  • Loading branch information
G0maa authored Apr 7, 2023
1 parent 69b68af commit 72978e8
Show file tree
Hide file tree
Showing 29 changed files with 361 additions and 115 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ test.drawio
A&D - MVP.md
out/
test.ts
snippet.ts
package-lock.json
coverage/
uploads/
Expand Down
11 changes: 8 additions & 3 deletions src/controllers/holiday.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,21 @@ import {
} from '../services/holiday.service';
import { setAuthorizedRoles, isAuthenticated } from '../utils/middleware';
import { ZRole } from '../validator/general.validator';
import { ZHolidayPost, ZHolidayDelete } from '../validator/holiday.validator';
import {
ZHolidayPost,
ZHolidayDelete,
ZHolidayFind,
} from '../validator/holiday.validator';

const holidayRouter = express.Router();

holidayRouter.get('/', isAuthenticated, async (_req, res) => {
holidayRouter.get('/', isAuthenticated, async (req, res) => {
/*
#swagger.tags = ['Holidays']
#swagger.security = [{ "cookieAuth": [] }]
*/
const holidays = await getHolidays();
const { query } = ZHolidayFind.parse(req);
const holidays = await getHolidays(query);

return res.status(200).json(holidays).end();
});
Expand Down
10 changes: 7 additions & 3 deletions src/controllers/subjectsMaterial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { ZRole } from '../validator/general.validator';
import { ZSubjectGetOne } from '../validator/subject.validator';
import {
ZSubjectsMaterialFind,
ZSubjectsMaterialOne,
ZSubjectsMaterialPost,
} from '../validator/subjectsMaterial.validator';
Expand All @@ -24,14 +25,17 @@ const subjectMaterialRouter = express.Router();

// Needs testing
// This router is very experimental, lots of validations & verifications are missing.
subjectMaterialRouter.get('/', isAuthenticated, async (_req, res) => {
subjectMaterialRouter.get('/', isAuthenticated, async (req, res) => {
/*
#swagger.tags = ['Subjects Material']
#swagger.security = [{ "cookieAuth": [] }]
*/
const query = await getSubjectsMaterial();

return res.status(200).json(query).end();
const { query } = ZSubjectsMaterialFind.parse(req);

const result = await getSubjectsMaterial(query);

return res.status(200).json(result).end();
});

// Download, needs some love.
Expand Down
15 changes: 12 additions & 3 deletions src/services/activeSubject.service.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-misused-promises */
import ActiveSubject from '../models/activeSubject';
import { getPagination, querifyStringFields } from '../utils/helpers';
import {
ZActiveSubject,
ZActiveSubjectDelete,
ZActiveSubjectFind,
ZActiveSubjectGet,
ZActiveSubjectQuery,
} from '../validator/activeSubject.validator';

// Searching not tested
const getActiveSubjects = async (searchQuery: ZActiveSubjectQuery) => {
const query = await ActiveSubject.findAll({ where: { ...searchQuery } });
const getActiveSubjects = async (searchQuery: ZActiveSubjectFind['query']) => {
const { offset, limit, rest } = getPagination(searchQuery);

const querified = querifyStringFields(rest, ZActiveSubjectFind.shape.query);

const query = await ActiveSubject.findAndCountAll({
where: querified,
offset,
limit,
});
return query;
};

Expand Down
14 changes: 12 additions & 2 deletions src/services/fee.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
import Fee from '../models/fee';
import { getPagination, querifyStringFields } from '../utils/helpers';
import { ZFee, ZFeeFind, ZFeePut } from '../validator/fee.validator';
import { ZReqUser } from '../validator/user.validator';

const getFees = async (zFeeFind: ZFeeFind['query'], user: ZReqUser) => {
const { offset, limit, rest } = getPagination(zFeeFind);

const querified = querifyStringFields(rest, ZFeeFind.shape.query);

let fees;
if (user.role === 'Admin')
fees = await Fee.findAll({ where: { ...zFeeFind } });
else fees = await Fee.findAll({ where: { ...zFeeFind, studentId: user.id } });
fees = await Fee.findAndCountAll({ where: querified, offset, limit });
else
fees = await Fee.findAndCountAll({
where: { ...querified, studentId: user.id },
offset,
limit,
});
return fees;
};

Expand Down
19 changes: 14 additions & 5 deletions src/services/grade.service.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
import ActiveSubject from '../models/activeSubject';
import Grade from '../models/grade';
import { getPagination, querifyStringFields } from '../utils/helpers';
import { ZGrade, ZGradeFind, ZGradePut } from '../validator/grade.validator';
import { ZReqUser } from '../validator/user.validator';

const getGrades = async (gradeFind: ZGradeFind['query'], user: ZReqUser) => {
const { offset, limit, rest } = getPagination(gradeFind);

const querified = querifyStringFields(rest, ZGradeFind.shape.query);

let grades;

if (user.role === 'Admin') {
grades = await Grade.findAll({ where: gradeFind });
grades = await Grade.findAndCountAll({ where: querified, offset, limit });
} else if (user.role === 'Student') {
grades = await Grade.findAll({
where: { ...gradeFind, studentId: user.id },
grades = await Grade.findAndCountAll({
where: { ...querified, studentId: user.id },
offset,
limit,
});
} else if (user.role === 'Teacher') {
grades = await Grade.findAll({
grades = await Grade.findAndCountAll({
include: [{ model: ActiveSubject, where: { teacherId: user.id } }],
where: { ...gradeFind },
attributes: { exclude: ['activeSubject'] },
where: querified,
offset,
limit,
});
}

Expand Down
13 changes: 10 additions & 3 deletions src/services/holiday.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import Holiday from '../models/holiday';
import { ZHolidayDelete, ZHolidayPost } from '../validator/holiday.validator';
import { getPagination } from '../utils/helpers';
import {
ZHolidayDelete,
ZHolidayFind,
ZHolidayPost,
} from '../validator/holiday.validator';

const getHolidays = async () => {
const holidays = await Holiday.findAll();
const getHolidays = async (query: ZHolidayFind['query']) => {
const { offset, limit } = getPagination(query);

const holidays = await Holiday.findAndCountAll({ offset, limit });
return holidays;
};

Expand Down
15 changes: 12 additions & 3 deletions src/services/studyClass.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import StudyClass from '../models/class';
import { getPagination, querifyStringFields } from '../utils/helpers';
import {
ZStudyClass,
ZStudyClassQuery,
ZStudyClassFind,
} from '../validator/studyClass.validator';

// Searching not well tested
const getStudyClasses = async (searchQuery: ZStudyClassQuery) => {
const query = await StudyClass.findAll({ where: { ...searchQuery } });
const getStudyClasses = async (searchQuery: ZStudyClassFind['query']) => {
const { offset, limit, rest } = getPagination(searchQuery);

const querified = querifyStringFields(rest, ZStudyClassFind.shape.query);

const query = await StudyClass.findAndCountAll({
where: querified,
offset,
limit,
});
return query;
};

Expand Down
26 changes: 21 additions & 5 deletions src/services/subject.service.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
import Subject from '../models/subject';
import { ZSubject, ZSubjectQuery } from '../validator/subject.validator';

const getSubjects = async (searchQuery: ZSubjectQuery) => {
const query = await Subject.findAll({
where: { ...searchQuery },
import { getPagination, querifyStringFields } from '../utils/helpers';
import { ZSubject, ZSubjectFind } from '../validator/subject.validator';

const getSubjects = async (searchQuery: ZSubjectFind['query']) => {
// searchQuery is opriginally in req.query
// searchQuery: {name: 'test', studyYear: '1', page: 1, size: 10}
// querified: {name: { [Op.like]: '%test%' }, studyYear: '1', page: 1, size: 10}
// paginated: {limit: 10, offset: 0}

// The way the functions are, forces a certain order,
// which is I dislike, queryifing wihtout getting rid of page & size,
// makes sequelize complain about unknown attributes.
const { offset, limit, rest } = getPagination(searchQuery);

// Note: searchQuery after being querified, still has page & size.
const querified = querifyStringFields(rest, ZSubjectFind.shape.query);

const query = await Subject.findAndCountAll({
where: querified,
offset,
limit,
});
return query;
};
Expand Down
19 changes: 17 additions & 2 deletions src/services/subjectsMaterial.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,26 @@ import SubjectsMaterial from '../models/subjectsMaterial';
import { ZSubject } from '../validator/subject.validator';
import {
ZSubjectsMaterial,
ZSubjectsMaterialFind,
ZSubjectsMaterialOne,
} from '../validator/subjectsMaterial.validator';
import { getPagination, querifyStringFields } from '../utils/helpers';

const getSubjectsMaterial = async () => {
const subjectMaterial = await SubjectsMaterial.findAll();
const getSubjectsMaterial = async (
searchQuery: ZSubjectsMaterialFind['query']
) => {
const { limit, offset, rest } = getPagination(searchQuery);

const querified = querifyStringFields(
rest,
ZSubjectsMaterialFind.shape.query
);

const subjectMaterial = await SubjectsMaterial.findAndCountAll({
where: querified,
offset,
limit,
});
return subjectMaterial;
};

Expand Down
19 changes: 16 additions & 3 deletions src/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
generateRandomPassword,
verifyPassword,
hashPassword,
querifyStringFields,
getPagination,
} from '../utils/helpers';
import { Role, ZUuid } from '../validator/general.validator';
import {
Expand Down Expand Up @@ -31,12 +33,23 @@ const getUsers = async (
userDetailsQuery: ZUserDetailsQuery,
roleQuery: ZStudentQuery | ZTeacherQuery
) => {
const { offset, limit, rest } = getPagination(userQuery);

const querifiedUserQuery = querifyStringFields(rest, ZUserQuery);
const querifiedUserDetailsQuery = querifyStringFields(
userDetailsQuery,
ZUserDetailsQuery
);
const querifiedRoleQuery = querifyStringFields(roleQuery, ZStudentQuery);

const query = await User.findAll({
include: [
{ model: UserDetails, where: { ...userDetailsQuery } },
{ model: roleModels[role], where: roleQuery },
{ model: UserDetails, where: querifiedUserDetailsQuery },
{ model: roleModels[role], where: querifiedRoleQuery },
],
where: { role, ...userQuery },
where: { role, ...querifiedUserQuery },
offset,
limit,
});
return query;
};
Expand Down
59 changes: 59 additions & 0 deletions src/tests/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Unit tests for the utils/helpers.ts file

import { Op } from 'sequelize';
import { getPagination, querifyStringFields } from '../utils/helpers';
import { ZSubjectFind } from '../validator/subject.validator';
import { ZStudyClassFind } from '../validator/studyClass.validator';

// This was made with the help of Github Copilot (first time using it)
describe('getPagination() function unit tests', () => {
test('should return an object with offset and limit', () => {
const obj = getPagination({ page: 1, size: 10 });

expect(obj).toHaveProperty('offset');
expect(obj).toHaveProperty('limit');

expect(obj.offset).toBe(0);
expect(obj.limit).toBe(10);
});

test('getPagination() returns rest of the object', () => {
const obj = getPagination({ page: 1, size: 10, name: 'test', age: 2 });

expect(obj.rest).toEqual({ name: 'test', age: 2 });
});
});

describe('querifyStringFields() function unit tests', () => {
test('Try on Subject model', () => {
// You can do it like this:
const { query } = ZSubjectFind.parse({
query: { name: 'test', subjectId: '123' },
});

const querified = querifyStringFields(query, ZSubjectFind.shape.query);

expect(querified.name).toStrictEqual({ [Op.like]: '%test%' });

// subjectId is ZodString, that's why it is "querified".
expect(querified.subjectId).toStrictEqual({ [Op.like]: '%123%' });
expect(querified.page).toBe(1);
expect(querified.size).toBe(10);
});

test('Try on StudyClass model', () => {
// or like this:
const query = ZStudyClassFind.shape.query.parse({
educationType: 'Literature',
studyYear: '1',
classId: '123',
});

const querified = querifyStringFields(query, ZStudyClassFind.shape.query);

// Enums stay the same.
expect(querified.educationType).toStrictEqual('Literature');
expect(querified.studyYear).toStrictEqual('1');
expect(querified.classId).toStrictEqual({ [Op.like]: '%123%' });
});
});
Loading

0 comments on commit 72978e8

Please sign in to comment.