Создание таблицы студентов. Работа с локальным сервером

Создал список студентов во фронтенд части. В бэкэнд - сервер. В данном случае добавление студентов в таблицу, их получение из сервера и удаление осуществляется через fetch-запросы к серверу. Объект студента во фронтенд части реализован через class. Столкнулся с проблемой не функционирования сортировки и фильтрации студентов. До взаимодействия с сервером функции работали. Взаимодействие с сервером осуществляется: студенты добавляются, сохраняются и удаляются. Но не работает сортировка и фильтрация. Возможно подскажете что в коде не то?

Фронтенд часть: (некоторые части закоментированы как предыдущие варианты реализации)

// Создание класса для массива студентов и методов работы с ним

class Student {
  constructor(id, name, surname, lastname, birthday, studyStart, faculty) {
    this.id = id,
    this.name = name;
    this.lastname = lastname;
    this.surname = surname;
    this.studyStart = studyStart;
    this.faculty = faculty;
    this.birthday = birthday;
  }

  get FullName() {
    return this.surname + " " + this.name + " " + this.lastname;
  }

  convertBirthDayString() {
    let day = this.birthday.getDate();
    day = day < 10 ? "0" + day : day;
    let month = this.birthday.getMonth() + 1;
    month = month < 10 ? "0" + month : month;
    const year = this.birthday.getFullYear();
    return day + "." + month + "." + year;
  }

  getAgeStudent() {
    const today = new Date();
    let age = today.getFullYear() - this.birthday.getFullYear();
    let m = today.getMonth() - this.birthday.getMonth();
    if (m < 0 || (m === 0 && today.getDate() < this.birthday.getDate())) {
      age--;
    }
    return ` (${age} лет)`;
  }

  getStudyTime() {
    let currentTime = new Date();
    // corse = today.getFullYear() -

    if (
      (currentTime.getMonth() + 1 >= 9 &&
        currentTime.getFullYear() - this.studyStart > 4) ||
      currentTime.getFullYear() - this.studyStart > 4
    ) {
      return `${+this.studyStart} - ${+this.studyStart + 4} (закончил)`;
    } else {
      return `${+this.studyStart} - ${+this.studyStart + 4} (${
        currentTime.getFullYear() - this.studyStart + 1
      }-й курс)`;
    }
  }
}

// Функция подсчета срока учебы на текущую дату перенесена в класс

// getStudiesPeriod() {
//   const currentTime = new Date();
//   return currentTime.getFullYear() - this.studyStart;
// }


const SERVER_URL = "http://localhost:3000";

// ***ФУНКЦИЯ ДОБАВЛЕНИЯ СТУДЕНТА через СЕРВЕР

async function serverAddStudent(obj) {
  let response = await fetch(SERVER_URL + "/api/students", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(obj),
  });

  let data = await response.json();
  return data;
}

// ФУНКЦИЯ получения студентов с сервера
async function serverGetStudent() {
  let response = await fetch(SERVER_URL + "/api/students", {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
    },
  });

  let data = await response.json();
  return data;
}

// УДАЛЕНИЕ СТУДЕНТА
async function serverDeleteStudent(id) {
  let response = await fetch(SERVER_URL + "/api/students/" + id, {
    method: "DELETE",
  });

  let data = await response.json();
  return data;
}

// let studentsList = [];
// (async () => {
//   let serverData = await serverGetStudent();
//   if (serverData) {
//     for (const student of serverData) {
//       studentsList.push(
//         new Student(
//           student.id,
//           student.name,
//           student.surname,
//           student.lastname,
//           new Date(student.birthday),
//           student.studyStart,
//           student.faculty
//         )
//       );
//     }
//   }
// })();

let studentsList = [];

serverGetStudent().then(students => {
  studentsList = students
  render();
})


// const $studentsList = document.getElementById("student-list");
const $studentsListTHALL = document.querySelectorAll(".studentTable th");
$filterForm = document.getElementById("filter-form");
$validationForm = document.getElementById("add-student");
$fullNameFilterInp = document.getElementById("filter-fio");
$facultyFilterInp = document.getElementById("filter-faculty");
$studyStartFilterInp = document.getElementById("filter-studyStart");
$studyFinishFilterInp = document.getElementById("filter-studyFinish");
($inputName = document.getElementById("input-name")),
  ($inputSurname = document.getElementById("input-surname")),
  ($inputLastname = document.getElementById("input-lastname")),
  ($inputFaculty = document.getElementById("input-faculty")),
  ($inputBirthday = new Date(document.getElementById("input-birthday"))),
  ($inputStudyStart = Number(document.getElementById("input-studyStart")));

let column = "FullName",
  columnDir = true;

// Получаем TR (таблицы) студентов
function newStudentTR(student) {
  const $studentTR = document.createElement("tr");
  ($fioTD = document.createElement("td")),
    ($birthdayTD = document.createElement("td")),
    ($facultyTD = document.createElement("td")),
    ($studyStartTD = document.createElement("td"));
  $deleteTD = document.createElement("td");
  $deleteBtn = document.createElement("button");

  $deleteBtn.classList.add("btn", "btn-danger");
  $deleteBtn.textContent = "Удалить";

  $fioTD.textContent = student.FullName;
  $facultyTD.textContent = student.faculty;
  $birthdayTD.textContent =
    student.convertBirthDayString() + student.getAgeStudent();
  $studyStartTD.textContent = student.getStudyTime();
  student.studyFinish = $studyStartTD.textContent.match(/\d+/g)[1];

  $deleteBtn.addEventListener("click", async function () {
    await serverDeleteStudent(student.id);
    $studentTR.remove();
  });

  $deleteTD.append($deleteBtn);
  $studentTR.append($fioTD);
  $studentTR.append($birthdayTD);
  $studentTR.append($facultyTD);
  $studentTR.append($studyStartTD);
  $studentTR.append($deleteTD);

  return $studentTR;
}

// Сортировка массива по параметрам
function getSortStudents(prop, dir) {
  const sortedStudentsList = [...studentsList];

  return sortedStudentsList.sort((studentA, studentB) =>
    !dir
      ? studentA[prop] > studentB[prop]
      : studentA[prop] < studentB[prop]
      ? -1
      : 1
  );
}

function filterStudents(studentsList, prop, value) {
  const filteredArr = [];
  const copyArr = [...studentsList];
  for (const student of copyArr) {
    if (String(student[prop]).includes(value)) filteredArr.push(student);
  }
  return filteredArr;
}

// Отрисовка таблицы студентов
async function render() {
  let studentsListCopy = [...studentsList];

  const $studentsList = document.getElementById("student-list");

  $studentsList.innerHTML = "";


  for (const student of studentsListCopy) {
    $studentsList.append(
      newStudentTR(
        new Student(
          student.id,
          student.name,
          student.surname,
          student.lastname,
          new Date(student.birthday),
          student.studyStart,
          student.faculty
        )
      )
    );
  }

  studentsListCopy = getSortStudents(column, columnDir);

  // События фильтрации
  const FullNameVal = document.getElementById("filter-fio").value;
  facultyVal = document.getElementById("filter-faculty").value;
  studyStartVal = document.getElementById("filter-studyStart").value;
  studyFinishVal = document.getElementById("filter-studyFinish").value;

  if (FullNameVal !== "")
    studentsListCopy = filterStudents(
      studentsListCopy,
      "FullName",
      FullNameVal
    );
  if (facultyVal !== "")
    studentsListCopy = filterStudents(studentsListCopy, "faculty", facultyVal);
  if (studyStartVal !== "" && studyStartVal.length > 3)
    studentsListCopy = filterStudents(
      studentsListCopy,
      "studyStart",
      studyStartVal
    );
  if (studyFinishVal !== "" && studyFinishVal.length > 3)
    studentsListCopy = filterStudents(
      studentsListCopy,
      "studyFinish",
      studyFinishVal
    );
}
render();

// События сортировки
$studentsListTHALL.forEach((element) => {
  element.addEventListener("click", function () {
    column = this.dataset.column;
    columnDir = !columnDir;
    render(studentsList);
  });
});

// Добавление
$validationForm.addEventListener("submit", async function (event) {
  event.preventDefault();

  let serverDataObj = await serverAddStudent({
    name: document.getElementById("input-name").value,
    surname: document.getElementById("input-surname").value,
    lastname: document.getElementById("input-lastname").value,
    birthday: new Date(document.getElementById("input-birthday").value),
    studyStart: Number(document.getElementById("input-studyStart").value),
    faculty: document.getElementById("input-faculty").value,
  });

  if (validation(this) == true) {
    alert("Форма успешно заполнена");
    studentsList.push(
      new Student(
        serverDataObj.id,
        serverDataObj.name,
        serverDataObj.surname,
        serverDataObj.lastname,
        new Date(serverDataObj.birthday),
        serverDataObj.studyStart,
        serverDataObj.faculty
      )
    );
  }

  render();
});

// ВАЛИДАЦИЯ ФОРМЫ
function validation($validationForm) {
  function removeError(input) {
    const parent = input.closest(".input-box");
    if (parent.classList.contains("error")) {
      parent.querySelector(".error-label").remove();
      parent.classList.remove("error");
    }
  }

  function createError(input, text) {
    const parent = input.closest(".input-box");
    const errorLabel = document.createElement("label");

    errorLabel.classList.add("error-label");
    errorLabel.textContent = text;

    parent.classList.add("error");

    parent.append(errorLabel);
  }

  let result = true;

  $validationForm.querySelectorAll("input").forEach((input) => {
    removeError(input);

    if (input.dataset.required == "true") {
      if (input.value == "") {
        removeError(input);
        createError(input, "Поле не заполнено!");
        result = false;
      }
    }

    if (input.dataset.minTerm) {
      if (input.value != "" && input.value < input.dataset.minTerm) {
        removeError(input);
        createError(
          input,
          `Введите год поступления, начиная с ${input.dataset.minTerm}`
        );
        result = false;
      }
    }
  });

  return result;
}

$filterForm.addEventListener("submit", function (event) {
  event.preventDefault();

  render();
});

$fullNameFilterInp.addEventListener("input", function () {
  render();
});
$facultyFilterInp.addEventListener("input", function () {
  render();
});
$studyStartFilterInp.addEventListener("input", function () {
  render();
});
$studyFinishFilterInp.addEventListener("input", function () {
  render();
});

render();

Бэкэнд часть

* eslint-disable no-console */
// импорт стандартных библиотек Node.js
const { existsSync, readFileSync, writeFileSync } = require('fs');
const { createServer } = require('http');

// файл для базы данных
const DB_FILE = process.env.DB_FILE || './db.json';
// номер порта, на котором будет запущен сервер
const PORT = process.env.PORT || 3000;
// префикс URI для всех методов приложения
const URI_PREFIX = '/api/students';

/**
 * Класс ошибки, используется для отправки ответа с определённым кодом и описанием ошибки
 */
class ApiError extends Error {
    constructor(statusCode, data) {
        super();
        this.statusCode = statusCode;
        this.data = data;
    }
}

/**
 * Асинхронно считывает тело запроса и разбирает его как JSON
 * @param {Object} req - Объект HTTP запроса
 * @throws {ApiError} Некорректные данные в аргументе
 * @returns {Object} Объект, созданный из тела запроса
 */
function drainJson(req) {
    return new Promise((resolve) => {
        let data = '';
        req.on('data', (chunk) => {
            data += chunk;
        });
        req.on('end', () => {
            resolve(JSON.parse(data));
        });
    });
}

/**
 * Проверяет входные данные и создаёт из них корректный объект студента
 * @param {Object} data - Объект с входными данными
 * @throws {ApiError} Некорректные данные в аргументе (statusCode 422)
 * @returns {{ name: string, surname: string, lastname: string, birthday: string, studyStart: string, faculty: string }} Объект студента
 */
function makeStudentFromData(data) {
    const errors = [];

    function asString(v) {
        return v && String(v).trim() || '';
    }

    // составляем объект, где есть только необходимые поля
    const student = {
        name: asString(data.name),
        surname: asString(data.surname),
        lastname: asString(data.lastname),
        birthday: asString(data.birthday),
        studyStart: asString(data.studyStart),
        faculty: asString(data.faculty),
    }

    // проверяем, все ли данные корректные и заполняем объект ошибок, которые нужно отдать клиенту
    if (!student.name) errors.push({ field: 'name', message: 'Не указано имя' });
    if (!student.surname) errors.push({ field: 'surname', message: 'Не указана фамилия' });
    if (!student.lastname) errors.push({ field: 'lastname', message: 'Не указано отчество' });

    if (!student.birthday) errors.push({ field: 'birthday', message: 'Не указана дата рождения' });
    if (!student.studyStart) errors.push({ field: 'studyStart', message: 'Не указано начало обучения' });
    if (!student.faculty) errors.push({ field: 'faculty', message: 'Не указан факультет' });

    // если есть ошибки, то бросаем объект ошибки с их списком и 422 статусом
    if (errors.length) throw new ApiError(422, { errors });

    return student;
}

/**
 * Возвращает список студентов из базы данных
 * @param {{ search: string }} [params] - Поисковая строка
 * @returns {{ id: string, name: string, surname: string, lastname: string, birthday: string, studyStart: string, faculty: string }[]} Массив студентов
 */
function getStudentList(params = {}) {
    const students = JSON.parse(readFileSync(DB_FILE) || '[]');
    if (params.search) {
        const search = params.search.trim().toLowerCase();
        return students.filter(student => [
                student.name,
                student.surname,
                student.lastname,
                student.birthday,
                student.studyStart,
                student.faculty,
            ]
            .some(str => str.toLowerCase().includes(search))
        );
    }
    return students;
}

/**
 * Создаёт и сохраняет студента в базу данных
 * @throws {ApiError} Некорректные данные в аргументе, студент не создан (statusCode 422)
 * @param {Object} data - Данные из тела запроса
 * @returns {{ id: string, name: string, surname: string, lastname: string, birthday: string, studyStart: string, faculty: string, createdAt: string, updatedAt: string }} Объект студента
 */
function createStudent(data) {
    const newItem = makeStudentFromData(data);
    newItem.id = Date.now().toString();
    newItem.createdAt = newItem.updatedAt = new Date().toISOString();
    writeFileSync(DB_FILE, JSON.stringify([...getStudentList(), newItem]), { encoding: 'utf8' });
    return newItem;
}

/**
 * Возвращает объект студента по его ID
 * @param {string} itemId - ID студента
 * @throws {ApiError} Студент с таким ID не найден (statusCode 404)
 * @returns {{ id: string, name: string, surname: string, lastname: string, birthday: string, studyStart: string, faculty: string, createdAt: string, updatedAt: string }} Объект студента
 */
function getStudent(itemId) {
    const student = getStudentList().find(({ id }) => id === itemId);
    if (!student) throw new ApiError(404, { message: 'Student Not Found' });
    return student;
}

/**
 * Изменяет студента с указанным ID и сохраняет изменения в базу данных
 * @param {string} itemId - ID изменяемого студента
 * @param {{ name?: string, surname?: string, lastname?: string, birthday?: string, studyStart?: string, faculty?: string }} data - Объект с изменяемыми данными
 * @throws {ApiError} Студент с таким ID не найден (statusCode 404)
 * @throws {ApiError} Некорректные данные в аргументе (statusCode 422)
 * @returns {{ id: string, name: string, surname: string, lastname: string, birthday: string, studyStart: string, faculty: string, createdAt: string, updatedAt: string }} Объект студента
 */
function updateStudent(itemId, data) {
    const students = getStudentList();
    const itemIndex = students.findIndex(({ id }) => id === itemId);
    if (itemIndex === -1) throw new ApiError(404, { message: 'Student Not Found' });
    Object.assign(students[itemIndex], makeStudentFromData({...students[itemIndex], ...data }));
    students[itemIndex].updatedAt = new Date().toISOString();
    writeFileSync(DB_FILE, JSON.stringify(students), { encoding: 'utf8' });
    return students[itemIndex];
}

/**
 * Удаляет студента из базы данных
 * @param {string} itemId - ID студента
 * @returns {{}}
 */
function deleteStudent(itemId) {
    const students = getStudentList();
    const itemIndex = students.findIndex(({ id }) => id === itemId);
    if (itemIndex === -1) throw new ApiError(404, { message: 'Student Not Found' });
    students.splice(itemIndex, 1);
    writeFileSync(DB_FILE, JSON.stringify(students), { encoding: 'utf8' });
    return {};
}

// создаём новый файл с базой данных, если он не существует
if (!existsSync(DB_FILE)) writeFileSync(DB_FILE, '[]', { encoding: 'utf8' });

// создаём HTTP сервер, переданная функция будет реагировать на все запросы к нему
module.exports = createServer(async(req, res) => {
        // req - объект с информацией о запросе, res - объект для управления отправляемым ответом

        // этот заголовок ответа указывает, что тело ответа будет в JSON формате
        res.setHeader('Content-Type', 'application/json');

        // CORS заголовки ответа для поддержки кросс-доменных запросов из браузера
        res.setHeader('Access-Control-Allow-Origin', '*');
        res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE, OPTIONS');
        res.setHeader('Access-Control-Allow-Headers', 'Content-Type');

        // запрос с методом OPTIONS может отправлять браузер автоматически для проверки CORS заголовков
        // в этом случае достаточно ответить с пустым телом и этими заголовками
        if (req.method === 'OPTIONS') {
            // end = закончить формировать ответ и отправить его клиенту
            res.end();
            return;
        }

        // если URI не начинается с нужного префикса - можем сразу отдать 404
        if (!req.url || !req.url.startsWith(URI_PREFIX)) {
            res.statusCode = 404;
            res.end(JSON.stringify({ message: 'Not Found' }));
            return;
        }

        // убираем из запроса префикс URI, разбиваем его на путь и параметры
        const [uri, query] = req.url.substr(URI_PREFIX.length).split('?');
        const queryParams = {};

        // параметры могут отсутствовать вообще или иметь вид a=b&b=c
        // во втором случае наполняем объект queryParams { a: 'b', b: 'c' }
        if (query) {
            for (const piece of query.split('&')) {
                const [key, value] = piece.split('=');
                queryParams[key] = value ? decodeURIComponent(value) : '';
            }
        }

        try {
            // обрабатываем запрос и формируем тело ответа
            const body = await (async() => {
                if (uri === '' || uri === '/') {
                    // /api/students
                    if (req.method === 'GET') return getStudentList(queryParams);
                    if (req.method === 'POST') {
                        const createdItem = createStudent(await drainJson(req));
                        res.statusCode = 201;
                        res.setHeader('Access-Control-Expose-Headers', 'Location');
                        res.setHeader('Location', `${URI_PREFIX}/${createdItem.id}`);
                        return createdItem;
                    }
                } else {
                    // /api/students/{id}
                    // параметр {id} из URI запроса
                    const itemId = uri.substr(1);
                    if (req.method === 'GET') return getStudent(itemId);
                    if (req.method === 'PATCH') return updateStudent(itemId, await drainJson(req));
                    if (req.method === 'DELETE') return deleteStudent(itemId);
                }
                return null;
            })();
            res.end(JSON.stringify(body));
        } catch (err) {
            // обрабатываем сгенерированную нами же ошибку
            if (err instanceof ApiError) {
                res.writeHead(err.statusCode);
                res.end(JSON.stringify(err.data));
            } else {
                // если что-то пошло не так - пишем об этом в консоль и возвращаем 500 ошибку сервера
                res.statusCode = 500;
                res.end(JSON.stringify({ message: 'Server Error' }));
                console.error(err);
            }
        }
    })
    // выводим инструкцию, как только сервер запустился...
    .on('listening', () => {
        if (process.env.NODE_ENV !== 'test') {
            console.log(`Сервер Students запущен. Вы можете использовать его по адресу http://localhost:${PORT}`);
            console.log('Нажмите CTRL+C, чтобы остановить сервер');
            console.log('Доступные методы:');
            console.log(`GET ${URI_PREFIX} - получить список студентов, в query параметр search можно передать поисковый запрос`);
            console.log(`POST ${URI_PREFIX} - создать студента, в теле запроса нужно передать объект { name: string, surname: string, lastname: string, birthday: string, studyStart: string, faculty: string}`);
            console.log(`GET ${URI_PREFIX}/{id} - получить студента по его ID`);
            console.log(`PATCH ${URI_PREFIX}/{id} - изменить студента с ID, в теле запроса нужно передать объект { name?: string, surname?: string, lastname?: string, birthday?: string, studyStart?: string, faculty?: string}`);
            console.log(`DELETE ${URI_PREFIX}/{id} - удалить студента по ID`);
        }
    })
    // ...и вызываем запуск сервера на указанном порту
    .listen(PORT);

Ответы (1 шт):

Автор решения: Михаил Горбунов

На самом деле все было просто: фильтрация и сортировка должны происходить до отображения всех tr в функции render() (отрисовка таблицы студентов).

Цикл for должен быть после всех присвоений данных в studentsListCopy().

→ Ссылка