|
@@ -1,266 +0,0 @@
|
|
-import { z } from "zod";
|
|
|
|
-import validatorjs from "validator";
|
|
|
|
-import type { Dayjs } from "#dayjs";
|
|
|
|
-import { dayjs } from "#dayjs";
|
|
|
|
-import { Validator } from "#api/v_0.1.0/types/custom-fields-types.js";
|
|
|
|
-type Validator = z.infer<typeof Validator>;
|
|
|
|
-
|
|
|
|
-// утилиты
|
|
|
|
-function validateLeastYearsAgo(date: string, years: number): boolean {
|
|
|
|
- // 1. Проверяем, что minAge является неотрицательным числом
|
|
|
|
- if (typeof years !== "number" || years < 0 || !Number.isInteger(years)) {
|
|
|
|
- console.error("Invalid years: must be a non-negative integer.");
|
|
|
|
- return false;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- // 2. Парсим дату рождения.
|
|
|
|
- // Третий аргумент `true` включает строгий режим парсинга для указанного формата.
|
|
|
|
- // Это означает, что дата должна точно соответствовать 'YYYY-MM-DD'.
|
|
|
|
- const birthDate: Dayjs = dayjs(date, "DD.MM.YYYY", true);
|
|
|
|
-
|
|
|
|
- // 3. Проверяем валидность даты.
|
|
|
|
- if (!birthDate.isValid()) {
|
|
|
|
- console.warn(`Invalid date format or non-existent date provided: ${date}`);
|
|
|
|
- return false;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- // 4. Получаем текущую дату.
|
|
|
|
- const today: Dayjs = dayjs();
|
|
|
|
-
|
|
|
|
- // 5. Проверяем, не является ли дата рождения будущей датой.
|
|
|
|
- if (birthDate.isAfter(today)) {
|
|
|
|
- console.warn(`Birth date ${date} cannot be in the future.`);
|
|
|
|
- return false;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- const calculatedAge: number = today.diff(birthDate, "year");
|
|
|
|
-
|
|
|
|
- // 7. Сравниваем рассчитанный возраст с минимально допустимым.
|
|
|
|
- return calculatedAge >= years;
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-const getTypeError = (expected: "string" | "number" | "date" | "audio") => {
|
|
|
|
- const types = {
|
|
|
|
- string: "строкой",
|
|
|
|
- number: "числом",
|
|
|
|
- date: "датой",
|
|
|
|
- audio: "аудио файлом",
|
|
|
|
- };
|
|
|
|
- return `Поле должно быть ${types[expected]}`;
|
|
|
|
-};
|
|
|
|
-const parseValidatorValue = (value: unknown): number | null => {
|
|
|
|
- const num = Number(value);
|
|
|
|
- return Number.isFinite(num) ? num : null;
|
|
|
|
-};
|
|
|
|
-
|
|
|
|
-// Декларация для поддержки webkitAudioContext в TypeScript
|
|
|
|
-declare global {
|
|
|
|
- interface Window {
|
|
|
|
- webkitAudioContext?: typeof AudioContext;
|
|
|
|
- }
|
|
|
|
-}
|
|
|
|
-async function getAudioDuration(file: File): Promise<number> {
|
|
|
|
- // Проверка на окружение браузера
|
|
|
|
- if (
|
|
|
|
- typeof window !== "undefined" &&
|
|
|
|
- typeof window.document !== "undefined" &&
|
|
|
|
- typeof URL !== "undefined" &&
|
|
|
|
- typeof Audio !== "undefined"
|
|
|
|
- ) {
|
|
|
|
- return new Promise<number>((resolve, reject) => {
|
|
|
|
- const audio = new Audio();
|
|
|
|
- // Устанавливаем preload в 'metadata', чтобы загрузить только метаданные, а не весь файл
|
|
|
|
- audio.preload = "metadata";
|
|
|
|
- const objectUrl = URL.createObjectURL(file);
|
|
|
|
-
|
|
|
|
- const cleanup = () => {
|
|
|
|
- URL.revokeObjectURL(objectUrl);
|
|
|
|
- // Удаляем обработчики, чтобы избежать утечек памяти и повторных вызовов
|
|
|
|
- audio.onloadedmetadata = null;
|
|
|
|
- audio.onerror = null;
|
|
|
|
- };
|
|
|
|
-
|
|
|
|
- audio.onloadedmetadata = () => {
|
|
|
|
- cleanup();
|
|
|
|
- if (typeof audio.duration === "number" && isFinite(audio.duration)) {
|
|
|
|
- resolve(audio.duration);
|
|
|
|
- } else {
|
|
|
|
- // Иногда duration может быть Infinity, если файл не может быть корректно прочитан
|
|
|
|
- reject(new Error("Audio duration is invalid or infinite."));
|
|
|
|
- }
|
|
|
|
- };
|
|
|
|
-
|
|
|
|
- audio.onerror = (e) => {
|
|
|
|
- cleanup();
|
|
|
|
- let errorMessage = "Error loading audio metadata in browser.";
|
|
|
|
- // Попытка получить более детальную информацию об ошибке
|
|
|
|
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
- if (e && (e as any).target && (e as any).target.error) {
|
|
|
|
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
- const mediaError = (e as any).target.error as MediaError;
|
|
|
|
- switch (mediaError.code) {
|
|
|
|
- case MediaError.MEDIA_ERR_ABORTED:
|
|
|
|
- errorMessage += " Fetch aborted by user.";
|
|
|
|
- break;
|
|
|
|
- case MediaError.MEDIA_ERR_NETWORK:
|
|
|
|
- errorMessage += " Network error.";
|
|
|
|
- break;
|
|
|
|
- case MediaError.MEDIA_ERR_DECODE:
|
|
|
|
- errorMessage += " Decode error or unsupported format.";
|
|
|
|
- break;
|
|
|
|
- case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED:
|
|
|
|
- errorMessage += " Source format not supported.";
|
|
|
|
- break;
|
|
|
|
- default:
|
|
|
|
- errorMessage += " Unknown media error.";
|
|
|
|
- }
|
|
|
|
- } else if (e instanceof Event && typeof e.type === "string") {
|
|
|
|
- // Общий случай ошибки события
|
|
|
|
- errorMessage += ` Event type: ${e.type}.`;
|
|
|
|
- }
|
|
|
|
- reject(new Error(errorMessage));
|
|
|
|
- };
|
|
|
|
-
|
|
|
|
- audio.src = objectUrl;
|
|
|
|
- // audio.load(); // Обычно не требуется, т.к. установка src запускает загрузку
|
|
|
|
- });
|
|
|
|
- }
|
|
|
|
- // Проверка на окружение Node.js
|
|
|
|
- else if (
|
|
|
|
- typeof process !== "undefined" &&
|
|
|
|
- process.versions &&
|
|
|
|
- process.versions.node
|
|
|
|
- ) {
|
|
|
|
- try {
|
|
|
|
- // Динамический импорт, чтобы music-metadata не пытался загрузиться в браузере
|
|
|
|
- const { parseBuffer } = await import("music-metadata");
|
|
|
|
-
|
|
|
|
- // File API предоставляет arrayBuffer(), который мы конвертируем в Buffer Node.js
|
|
|
|
- const arrayBuffer = await file.arrayBuffer();
|
|
|
|
- const buffer = Buffer.from(arrayBuffer);
|
|
|
|
-
|
|
|
|
- // parseBuffer ожидает Buffer и опционально mime-тип и размер
|
|
|
|
- // file.type и file.size приходят из объекта File
|
|
|
|
- const metadata = await parseBuffer(
|
|
|
|
- buffer,
|
|
|
|
- { mimeType: file.type, size: file.size },
|
|
|
|
- { duration: true },
|
|
|
|
- );
|
|
|
|
-
|
|
|
|
- if (metadata.format && typeof metadata.format.duration === "number") {
|
|
|
|
- return metadata.format.duration;
|
|
|
|
- } else {
|
|
|
|
- throw new Error(
|
|
|
|
- "Duration not found in audio metadata using music-metadata.",
|
|
|
|
- );
|
|
|
|
- }
|
|
|
|
- } catch (error) {
|
|
|
|
- const message = error instanceof Error ? error.message : String(error);
|
|
|
|
- // Перебрасываем ошибку, чтобы ее можно было отловить выше
|
|
|
|
- throw new Error(`Error processing audio in Node.js: ${message}`);
|
|
|
|
- }
|
|
|
|
- } else {
|
|
|
|
- // Неподдерживаемое окружение
|
|
|
|
- return Promise.reject(
|
|
|
|
- new Error(
|
|
|
|
- "Unsupported environment: Not browser or Node.js, or required APIs are missing.",
|
|
|
|
- ),
|
|
|
|
- );
|
|
|
|
- }
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-//
|
|
|
|
-//
|
|
|
|
-//
|
|
|
|
-//
|
|
|
|
-export const getValidationFunc = (
|
|
|
|
- validator: Validator,
|
|
|
|
-): ((v: unknown) => (string | true) | Promise<string | true>) | undefined => {
|
|
|
|
- const { code, value } = validator;
|
|
|
|
-
|
|
|
|
- switch (validator.code) {
|
|
|
|
- case "required":
|
|
|
|
- return (v: unknown) =>
|
|
|
|
- v !== undefined && v !== null && v !== ""
|
|
|
|
- ? true
|
|
|
|
- : "Поле должно быть заполнено";
|
|
|
|
-
|
|
|
|
- // Строковые валидаторы
|
|
|
|
- case "maxString":
|
|
|
|
- case "minString": {
|
|
|
|
- const parsedValue = parseValidatorValue(value);
|
|
|
|
- if (parsedValue === null) {
|
|
|
|
- return () => `Некорректное значение валидатора: ${value}`;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- return (v: unknown) => {
|
|
|
|
- if (v === null) return true;
|
|
|
|
- if (typeof v !== "string") return getTypeError("string");
|
|
|
|
- return code === "maxString"
|
|
|
|
- ? v.length <= parsedValue || `Максимум ${parsedValue} символов`
|
|
|
|
- : v.length >= parsedValue || `Минимум ${parsedValue} символов`;
|
|
|
|
- };
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- case "isMail":
|
|
|
|
- return (v: unknown) => {
|
|
|
|
- if (v === null) return true;
|
|
|
|
- if (typeof v !== "string") return getTypeError("string");
|
|
|
|
- return validatorjs.isEmail(v) || "Введите корректную почту";
|
|
|
|
- };
|
|
|
|
-
|
|
|
|
- // date
|
|
|
|
- case "leastYearsAgo": {
|
|
|
|
- const parsedValue = parseValidatorValue(value);
|
|
|
|
- if (parsedValue === null) {
|
|
|
|
- return () => `Некорректное значение валидатора: ${value}`;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- return (v: unknown) => {
|
|
|
|
- if (v === null) return true;
|
|
|
|
- if (typeof v !== "string") return getTypeError("string");
|
|
|
|
- return (
|
|
|
|
- validateLeastYearsAgo(v, parsedValue) || `Минимум ${parsedValue} лет}`
|
|
|
|
- );
|
|
|
|
- };
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- // Числовые валидаторы
|
|
|
|
- case "max":
|
|
|
|
- case "min": {
|
|
|
|
- const parsedValue = parseValidatorValue(value);
|
|
|
|
- if (parsedValue === null) {
|
|
|
|
- return () => `Некорректное значение валидатора: ${value}`;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- return (v: unknown) => {
|
|
|
|
- if (v === null) return true;
|
|
|
|
- if (typeof v !== "number") return getTypeError("number");
|
|
|
|
- return code === "max"
|
|
|
|
- ? v <= parsedValue || `Не больше ${parsedValue}`
|
|
|
|
- : v >= parsedValue || `Не меньше ${parsedValue}`;
|
|
|
|
- };
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- // Музыка
|
|
|
|
- case "audioMaxSec": {
|
|
|
|
- const parsedValue = parseValidatorValue(value);
|
|
|
|
- if (parsedValue === null) {
|
|
|
|
- return () => `Некорректное значение валидатора: ${value}`;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- return async (v: unknown) => {
|
|
|
|
- if (v === null) return true;
|
|
|
|
- if (!(v instanceof File)) return getTypeError("audio");
|
|
|
|
- const duration = await getAudioDuration(v);
|
|
|
|
- return (
|
|
|
|
- duration <= parsedValue ||
|
|
|
|
- `Превышен лимит длительности ${parsedValue} секунд`
|
|
|
|
- );
|
|
|
|
- };
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- default:
|
|
|
|
- return () => `Неизвестный валидатор: ${code}`;
|
|
|
|
- }
|
|
|
|
-};
|
|
|