aggiunti i file di validazione del frontend
This commit is contained in:
parent
10f87d21d8
commit
ad00749aa5
5 changed files with 885 additions and 0 deletions
244
src/validation/cliente.ts
Normal file
244
src/validation/cliente.ts
Normal file
|
|
@ -0,0 +1,244 @@
|
||||||
|
import { CLIENTE_CONSTRAINTS } from "./constraints";
|
||||||
|
import {
|
||||||
|
buildResult,
|
||||||
|
normalizeEmail,
|
||||||
|
normalizeString,
|
||||||
|
validateEmailFormat,
|
||||||
|
validateMaxLength,
|
||||||
|
validateRequired,
|
||||||
|
type FieldErrors,
|
||||||
|
type ValidationResult,
|
||||||
|
} from "./primitives";
|
||||||
|
|
||||||
|
export interface ClienteInput {
|
||||||
|
nome: string;
|
||||||
|
cognome: string;
|
||||||
|
via: string;
|
||||||
|
comune: string;
|
||||||
|
provincia: string;
|
||||||
|
telefono: string;
|
||||||
|
email: string;
|
||||||
|
note: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeClienteInput(raw: Record<string, unknown>): ClienteInput {
|
||||||
|
const noteRaw = raw.note;
|
||||||
|
return {
|
||||||
|
nome: normalizeString(raw.nome),
|
||||||
|
cognome: normalizeString(raw.cognome),
|
||||||
|
via: normalizeString(raw.via),
|
||||||
|
comune: normalizeString(raw.comune),
|
||||||
|
provincia: normalizeString(raw.provincia).toUpperCase(),
|
||||||
|
telefono: normalizeString(raw.telefono),
|
||||||
|
email: normalizeEmail(raw.email),
|
||||||
|
note:
|
||||||
|
noteRaw === null || noteRaw === undefined || noteRaw === ""
|
||||||
|
? null
|
||||||
|
: normalizeString(noteRaw),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateProvincia(errors: FieldErrors, provincia: string): void {
|
||||||
|
if (!provincia) return;
|
||||||
|
if (provincia.length !== CLIENTE_CONSTRAINTS.provincia.maxLength) {
|
||||||
|
errors.provincia = "Provincia deve essere di 2 caratteri";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateClienteFields(
|
||||||
|
data: ClienteInput,
|
||||||
|
errors: FieldErrors,
|
||||||
|
options: { requireAll: boolean }
|
||||||
|
): void {
|
||||||
|
if (options.requireAll || data.nome !== undefined) {
|
||||||
|
validateRequired(errors, "nome", data.nome, "Nome obbligatorio");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"nome",
|
||||||
|
data.nome,
|
||||||
|
CLIENTE_CONSTRAINTS.nome.maxLength,
|
||||||
|
"Nome"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.requireAll || data.cognome !== undefined) {
|
||||||
|
validateRequired(errors, "cognome", data.cognome, "Cognome obbligatorio");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"cognome",
|
||||||
|
data.cognome,
|
||||||
|
CLIENTE_CONSTRAINTS.cognome.maxLength,
|
||||||
|
"Cognome"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.requireAll || data.via !== undefined) {
|
||||||
|
validateRequired(errors, "via", data.via, "Via obbligatoria");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"via",
|
||||||
|
data.via,
|
||||||
|
CLIENTE_CONSTRAINTS.via.maxLength,
|
||||||
|
"Via"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.requireAll || data.comune !== undefined) {
|
||||||
|
validateRequired(errors, "comune", data.comune, "Comune obbligatorio");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"comune",
|
||||||
|
data.comune,
|
||||||
|
CLIENTE_CONSTRAINTS.comune.maxLength,
|
||||||
|
"Comune"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.requireAll || data.provincia !== undefined) {
|
||||||
|
validateRequired(errors, "provincia", data.provincia, "Provincia obbligatoria");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"provincia",
|
||||||
|
data.provincia,
|
||||||
|
CLIENTE_CONSTRAINTS.provincia.maxLength,
|
||||||
|
"Provincia"
|
||||||
|
);
|
||||||
|
validateProvincia(errors, data.provincia);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.requireAll || data.telefono !== undefined) {
|
||||||
|
validateRequired(errors, "telefono", data.telefono, "Telefono obbligatorio");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"telefono",
|
||||||
|
data.telefono,
|
||||||
|
CLIENTE_CONSTRAINTS.telefono.maxLength,
|
||||||
|
"Telefono"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.requireAll || data.email !== undefined) {
|
||||||
|
validateRequired(errors, "email", data.email, "Email obbligatoria");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"email",
|
||||||
|
data.email,
|
||||||
|
CLIENTE_CONSTRAINTS.email.maxLength,
|
||||||
|
"Email"
|
||||||
|
);
|
||||||
|
validateEmailFormat(errors, "email", data.email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateClienteCreate(
|
||||||
|
raw: Record<string, unknown>
|
||||||
|
): ValidationResult<ClienteInput> {
|
||||||
|
const data = normalizeClienteInput(raw);
|
||||||
|
const errors: FieldErrors = {};
|
||||||
|
validateClienteFields(data, errors, { requireAll: true });
|
||||||
|
return buildResult(data, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateClienteUpdate(
|
||||||
|
raw: Record<string, unknown>
|
||||||
|
): ValidationResult<Partial<ClienteInput>> {
|
||||||
|
const errors: FieldErrors = {};
|
||||||
|
const data: Partial<ClienteInput> = {};
|
||||||
|
|
||||||
|
if ("nome" in raw) {
|
||||||
|
data.nome = normalizeString(raw.nome);
|
||||||
|
validateRequired(errors, "nome", data.nome, "Nome obbligatorio");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"nome",
|
||||||
|
data.nome,
|
||||||
|
CLIENTE_CONSTRAINTS.nome.maxLength,
|
||||||
|
"Nome"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("cognome" in raw) {
|
||||||
|
data.cognome = normalizeString(raw.cognome);
|
||||||
|
validateRequired(errors, "cognome", data.cognome, "Cognome obbligatorio");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"cognome",
|
||||||
|
data.cognome,
|
||||||
|
CLIENTE_CONSTRAINTS.cognome.maxLength,
|
||||||
|
"Cognome"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("via" in raw) {
|
||||||
|
data.via = normalizeString(raw.via);
|
||||||
|
validateRequired(errors, "via", data.via, "Via obbligatoria");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"via",
|
||||||
|
data.via,
|
||||||
|
CLIENTE_CONSTRAINTS.via.maxLength,
|
||||||
|
"Via"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("comune" in raw) {
|
||||||
|
data.comune = normalizeString(raw.comune);
|
||||||
|
validateRequired(errors, "comune", data.comune, "Comune obbligatorio");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"comune",
|
||||||
|
data.comune,
|
||||||
|
CLIENTE_CONSTRAINTS.comune.maxLength,
|
||||||
|
"Comune"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("provincia" in raw) {
|
||||||
|
data.provincia = normalizeString(raw.provincia).toUpperCase();
|
||||||
|
validateRequired(errors, "provincia", data.provincia, "Provincia obbligatoria");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"provincia",
|
||||||
|
data.provincia,
|
||||||
|
CLIENTE_CONSTRAINTS.provincia.maxLength,
|
||||||
|
"Provincia"
|
||||||
|
);
|
||||||
|
validateProvincia(errors, data.provincia);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("telefono" in raw) {
|
||||||
|
data.telefono = normalizeString(raw.telefono);
|
||||||
|
validateRequired(errors, "telefono", data.telefono, "Telefono obbligatorio");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"telefono",
|
||||||
|
data.telefono,
|
||||||
|
CLIENTE_CONSTRAINTS.telefono.maxLength,
|
||||||
|
"Telefono"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("email" in raw) {
|
||||||
|
data.email = normalizeEmail(raw.email);
|
||||||
|
validateRequired(errors, "email", data.email, "Email obbligatoria");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"email",
|
||||||
|
data.email,
|
||||||
|
CLIENTE_CONSTRAINTS.email.maxLength,
|
||||||
|
"Email"
|
||||||
|
);
|
||||||
|
validateEmailFormat(errors, "email", data.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("note" in raw) {
|
||||||
|
data.note =
|
||||||
|
raw.note === null || raw.note === "" ? null : normalizeString(raw.note);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(data).length === 0) {
|
||||||
|
errors._form = "Nessun campo da aggiornare";
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildResult(data, errors);
|
||||||
|
}
|
||||||
184
src/validation/consegna.ts
Normal file
184
src/validation/consegna.ts
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
import { CONSEGNA_CONSTRAINTS } from "./constraints";
|
||||||
|
import {
|
||||||
|
buildResult,
|
||||||
|
normalizePositiveInt,
|
||||||
|
normalizeString,
|
||||||
|
validateDate,
|
||||||
|
validateEnum,
|
||||||
|
validateMaxLength,
|
||||||
|
validatePositiveInt,
|
||||||
|
validateRequired,
|
||||||
|
type FieldErrors,
|
||||||
|
type ValidationResult,
|
||||||
|
} from "./primitives";
|
||||||
|
|
||||||
|
export type StatoConsegna =
|
||||||
|
(typeof CONSEGNA_CONSTRAINTS.stato.allowedValues)[number];
|
||||||
|
|
||||||
|
export interface ConsegnaInput {
|
||||||
|
dataRitiro: string;
|
||||||
|
dataConsegna: string;
|
||||||
|
stato: StatoConsegna;
|
||||||
|
chiaveConsegna: string;
|
||||||
|
clienteId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function todayAtMidnight(): Date {
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
return today;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateDateNotRetroactive(
|
||||||
|
errors: FieldErrors,
|
||||||
|
field: string,
|
||||||
|
value: string,
|
||||||
|
label: string
|
||||||
|
): void {
|
||||||
|
if (!value || errors[field]) return;
|
||||||
|
|
||||||
|
const parsed = new Date(`${value}T00:00:00`);
|
||||||
|
if (parsed < todayAtMidnight()) {
|
||||||
|
errors[field] = `${label} non può essere anteriore alla data odierna`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeConsegnaInput(raw: Record<string, unknown>): ConsegnaInput {
|
||||||
|
return {
|
||||||
|
dataRitiro: normalizeString(raw.dataRitiro),
|
||||||
|
dataConsegna: normalizeString(raw.dataConsegna),
|
||||||
|
stato: normalizeString(raw.stato || "in deposito") as StatoConsegna,
|
||||||
|
chiaveConsegna: normalizeString(raw.chiaveConsegna).toUpperCase(),
|
||||||
|
clienteId: normalizePositiveInt(raw.clienteId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateConsegnaFields(
|
||||||
|
raw: Record<string, unknown>,
|
||||||
|
data: ConsegnaInput,
|
||||||
|
errors: FieldErrors,
|
||||||
|
options: { requireAll: boolean }
|
||||||
|
): void {
|
||||||
|
if (options.requireAll || "dataRitiro" in raw) {
|
||||||
|
validateRequired(errors, "dataRitiro", data.dataRitiro, "Data ritiro obbligatoria");
|
||||||
|
validateDate(errors, "dataRitiro", data.dataRitiro, "Data ritiro");
|
||||||
|
validateDateNotRetroactive(errors, "dataRitiro", data.dataRitiro, "Data ritiro");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.requireAll || "dataConsegna" in raw) {
|
||||||
|
validateRequired(
|
||||||
|
errors,
|
||||||
|
"dataConsegna",
|
||||||
|
data.dataConsegna,
|
||||||
|
"Data consegna obbligatoria"
|
||||||
|
);
|
||||||
|
validateDate(errors, "dataConsegna", data.dataConsegna, "Data consegna");
|
||||||
|
validateDateNotRetroactive(errors, "dataConsegna", data.dataConsegna, "Data consegna");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.requireAll || "stato" in raw) {
|
||||||
|
validateRequired(errors, "stato", data.stato, "Stato obbligatorio");
|
||||||
|
validateEnum(
|
||||||
|
errors,
|
||||||
|
"stato",
|
||||||
|
data.stato,
|
||||||
|
CONSEGNA_CONSTRAINTS.stato.allowedValues,
|
||||||
|
"Stato"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.requireAll || "chiaveConsegna" in raw) {
|
||||||
|
validateRequired(
|
||||||
|
errors,
|
||||||
|
"chiaveConsegna",
|
||||||
|
data.chiaveConsegna,
|
||||||
|
"Chiave consegna obbligatoria"
|
||||||
|
);
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"chiaveConsegna",
|
||||||
|
data.chiaveConsegna,
|
||||||
|
CONSEGNA_CONSTRAINTS.chiaveConsegna.maxLength,
|
||||||
|
"Chiave consegna"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.requireAll || "clienteId" in raw) {
|
||||||
|
validatePositiveInt(errors, "clienteId", data.clienteId, "Cliente", options.requireAll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateConsegnaCreate(
|
||||||
|
raw: Record<string, unknown>
|
||||||
|
): ValidationResult<ConsegnaInput> {
|
||||||
|
const data = normalizeConsegnaInput(raw);
|
||||||
|
const errors: FieldErrors = {};
|
||||||
|
validateConsegnaFields(raw, data, errors, { requireAll: true });
|
||||||
|
return buildResult(data, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateConsegnaUpdate(
|
||||||
|
raw: Record<string, unknown>
|
||||||
|
): ValidationResult<Partial<ConsegnaInput>> {
|
||||||
|
const errors: FieldErrors = {};
|
||||||
|
const data: Partial<ConsegnaInput> = {};
|
||||||
|
|
||||||
|
if ("dataRitiro" in raw) {
|
||||||
|
data.dataRitiro = normalizeString(raw.dataRitiro);
|
||||||
|
validateRequired(errors, "dataRitiro", data.dataRitiro, "Data ritiro obbligatoria");
|
||||||
|
validateDate(errors, "dataRitiro", data.dataRitiro, "Data ritiro");
|
||||||
|
validateDateNotRetroactive(errors, "dataRitiro", data.dataRitiro, "Data ritiro");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("dataConsegna" in raw) {
|
||||||
|
data.dataConsegna = normalizeString(raw.dataConsegna);
|
||||||
|
validateRequired(
|
||||||
|
errors,
|
||||||
|
"dataConsegna",
|
||||||
|
data.dataConsegna,
|
||||||
|
"Data consegna obbligatoria"
|
||||||
|
);
|
||||||
|
validateDate(errors, "dataConsegna", data.dataConsegna, "Data consegna");
|
||||||
|
validateDateNotRetroactive(errors, "dataConsegna", data.dataConsegna, "Data consegna");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("stato" in raw) {
|
||||||
|
data.stato = normalizeString(raw.stato) as StatoConsegna;
|
||||||
|
validateRequired(errors, "stato", data.stato, "Stato obbligatorio");
|
||||||
|
validateEnum(
|
||||||
|
errors,
|
||||||
|
"stato",
|
||||||
|
data.stato,
|
||||||
|
CONSEGNA_CONSTRAINTS.stato.allowedValues,
|
||||||
|
"Stato"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("chiaveConsegna" in raw) {
|
||||||
|
data.chiaveConsegna = normalizeString(raw.chiaveConsegna).toUpperCase();
|
||||||
|
validateRequired(
|
||||||
|
errors,
|
||||||
|
"chiaveConsegna",
|
||||||
|
data.chiaveConsegna,
|
||||||
|
"Chiave consegna obbligatoria"
|
||||||
|
);
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"chiaveConsegna",
|
||||||
|
data.chiaveConsegna,
|
||||||
|
CONSEGNA_CONSTRAINTS.chiaveConsegna.maxLength,
|
||||||
|
"Chiave consegna"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("clienteId" in raw) {
|
||||||
|
data.clienteId = normalizePositiveInt(raw.clienteId);
|
||||||
|
validatePositiveInt(errors, "clienteId", data.clienteId, "Cliente");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(data).length === 0) {
|
||||||
|
errors._form = "Nessun campo da aggiornare";
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildResult(data, errors);
|
||||||
|
}
|
||||||
39
src/validation/constraints.ts
Normal file
39
src/validation/constraints.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
//===================================================
|
||||||
|
// File: constraints.ts
|
||||||
|
// Vincoli derivati da initdb/01_schema.sql.
|
||||||
|
// author: "villari.andrea@libero.it"
|
||||||
|
// version: "1.0.0 2026-06-24"
|
||||||
|
//===================================================
|
||||||
|
|
||||||
|
export const UTENTE_CONSTRAINTS = {
|
||||||
|
nome: { maxLength: 100, required: true },
|
||||||
|
cognome: { maxLength: 100, required: true },
|
||||||
|
email: { maxLength: 255, required: true },
|
||||||
|
password: { minLength: 8, maxLength: 72, required: true },
|
||||||
|
ruolo: {
|
||||||
|
allowedValues: ["Operatore", "Amministratore"],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const CLIENTE_CONSTRAINTS = {
|
||||||
|
nome: { maxLength: 100, required: true },
|
||||||
|
cognome: { maxLength: 100, required: true },
|
||||||
|
via: { maxLength: 100, required: true },
|
||||||
|
comune: { maxLength: 100, required: true },
|
||||||
|
provincia: { maxLength: 2, required: true },
|
||||||
|
telefono: { maxLength: 30, required: true },
|
||||||
|
email: { maxLength: 255, required: true },
|
||||||
|
note: { required: false },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const CONSEGNA_CONSTRAINTS = {
|
||||||
|
dataconsegna: { required: true },
|
||||||
|
dataritiro: { required: true },
|
||||||
|
stato: {
|
||||||
|
allowedValues: ["da ritirare", "in consegna", "in giacenza", "in deposito"],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
chiaveConsegna: { maxLength: 8, required: true },
|
||||||
|
clienteId: { required: true },
|
||||||
|
} as const;
|
||||||
198
src/validation/primitives.ts
Normal file
198
src/validation/primitives.ts
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
//==========================================
|
||||||
|
// File: primitives.ts
|
||||||
|
// Funzioni di validazione dei dati prmitivi
|
||||||
|
// author: "villari.andrea@libero.it"
|
||||||
|
// version: "1.0.0 2026-06-18"
|
||||||
|
//==========================================
|
||||||
|
|
||||||
|
export type FieldErrors = Record<string, string>;
|
||||||
|
|
||||||
|
export type ValidationResult<T> =
|
||||||
|
| { success: true; data: T }
|
||||||
|
| { success: false; errors: FieldErrors };
|
||||||
|
|
||||||
|
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
|
||||||
|
export function normalizeString(value: unknown): string {
|
||||||
|
return value?.toString().trim() ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeEmail(value: unknown): string {
|
||||||
|
return normalizeString(value).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeBoolean(value: unknown, defaultValue = false): boolean {
|
||||||
|
if (value === true || value === "true") return true;
|
||||||
|
if (value === false || value === "false") return false;
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeDecimal(value: unknown): number {
|
||||||
|
if (typeof value === "number") return value;
|
||||||
|
const normalized = value?.toString().trim().replace(",", ".") ?? "";
|
||||||
|
if (!normalized) return Number.NaN;
|
||||||
|
return Number(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizePositiveInt(value: unknown): number {
|
||||||
|
if (typeof value === "number") return value;
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : Number.NaN;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeTime(value: unknown): string {
|
||||||
|
const str = normalizeString(value);
|
||||||
|
if (!str) return "";
|
||||||
|
if (/^\d{2}:\d{2}$/.test(str)) return `${str}:00`;
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateRequired(
|
||||||
|
errors: FieldErrors,
|
||||||
|
field: string,
|
||||||
|
value: string,
|
||||||
|
message: string
|
||||||
|
): void {
|
||||||
|
if (!value) errors[field] = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateMaxLength(
|
||||||
|
errors: FieldErrors,
|
||||||
|
field: string,
|
||||||
|
value: string,
|
||||||
|
maxLength: number,
|
||||||
|
label: string
|
||||||
|
): void {
|
||||||
|
if (value.length > maxLength) {
|
||||||
|
errors[field] = `${label} deve contenere al massimo ${maxLength} caratteri`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateMinLength(
|
||||||
|
errors: FieldErrors,
|
||||||
|
field: string,
|
||||||
|
value: string,
|
||||||
|
minLength: number,
|
||||||
|
label: string
|
||||||
|
): void {
|
||||||
|
if (value && value.length < minLength) {
|
||||||
|
errors[field] = `${label} deve contenere almeno ${minLength} caratteri`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateEmailFormat(
|
||||||
|
errors: FieldErrors,
|
||||||
|
field: string,
|
||||||
|
value: string
|
||||||
|
): void {
|
||||||
|
if (value && !EMAIL_REGEX.test(value)) {
|
||||||
|
errors[field] = "Formato email non valido";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateEnum(
|
||||||
|
errors: FieldErrors,
|
||||||
|
field: string,
|
||||||
|
value: string,
|
||||||
|
allowedValues: readonly string[],
|
||||||
|
label: string
|
||||||
|
): void {
|
||||||
|
if (value && !allowedValues.includes(value)) {
|
||||||
|
errors[field] = `${label} non valido`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateBoolean(
|
||||||
|
errors: FieldErrors,
|
||||||
|
field: string,
|
||||||
|
value: unknown,
|
||||||
|
label: string
|
||||||
|
): void {
|
||||||
|
if (value === undefined || value === null || value === "") return;
|
||||||
|
if (
|
||||||
|
value !== true &&
|
||||||
|
value !== false &&
|
||||||
|
value !== "true" &&
|
||||||
|
value !== "false"
|
||||||
|
) {
|
||||||
|
errors[field] = `${label} non valido`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
||||||
|
const TIME_REGEX = /^\d{2}:\d{2}:\d{2}$/;
|
||||||
|
|
||||||
|
export function validateDate(
|
||||||
|
errors: FieldErrors,
|
||||||
|
field: string,
|
||||||
|
value: string,
|
||||||
|
label: string
|
||||||
|
): void {
|
||||||
|
if (!value) return;
|
||||||
|
if (!DATE_REGEX.test(value)) {
|
||||||
|
errors[field] = `${label} non valida`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parsed = new Date(`${value}T00:00:00`);
|
||||||
|
if (Number.isNaN(parsed.getTime())) {
|
||||||
|
errors[field] = `${label} non valida`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateTimeFormat(
|
||||||
|
errors: FieldErrors,
|
||||||
|
field: string,
|
||||||
|
value: string,
|
||||||
|
label: string
|
||||||
|
): void {
|
||||||
|
if (!value) return;
|
||||||
|
if (!TIME_REGEX.test(value)) {
|
||||||
|
errors[field] = `${label} non valida`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validatePositiveInt(
|
||||||
|
errors: FieldErrors,
|
||||||
|
field: string,
|
||||||
|
value: number,
|
||||||
|
label: string,
|
||||||
|
required = true
|
||||||
|
): void {
|
||||||
|
if (Number.isNaN(value) || value <= 0) {
|
||||||
|
errors[field] = required ? `${label} obbligatorio` : `${label} non valido`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateTimeRange(
|
||||||
|
errors: FieldErrors,
|
||||||
|
_startField: string,
|
||||||
|
endField: string,
|
||||||
|
start: string,
|
||||||
|
end: string
|
||||||
|
): void {
|
||||||
|
if (!start || !end) return;
|
||||||
|
if (start >= end) {
|
||||||
|
errors[endField] =
|
||||||
|
"L'ora di fine deve essere successiva all'ora di inizio";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateDecimalPositive(
|
||||||
|
errors: FieldErrors,
|
||||||
|
field: string,
|
||||||
|
value: unknown,
|
||||||
|
label: string,
|
||||||
|
min = 0.01
|
||||||
|
): void {
|
||||||
|
if (value === undefined || value === null || value === "") return;
|
||||||
|
const num = typeof value === "number" ? value : Number(value);
|
||||||
|
if (Number.isNaN(num) || num < min) {
|
||||||
|
errors[field] = `${label} deve essere almeno ${min}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildResult<T>(data: T, errors: FieldErrors): ValidationResult<T> {
|
||||||
|
return Object.keys(errors).length > 0
|
||||||
|
? { success: false, errors }
|
||||||
|
: { success: true, data };
|
||||||
|
}
|
||||||
220
src/validation/utente.ts
Normal file
220
src/validation/utente.ts
Normal file
|
|
@ -0,0 +1,220 @@
|
||||||
|
import { UTENTE_CONSTRAINTS } from "./constraints";
|
||||||
|
import {
|
||||||
|
buildResult,
|
||||||
|
normalizeEmail,
|
||||||
|
normalizeString,
|
||||||
|
validateEmailFormat,
|
||||||
|
validateEnum,
|
||||||
|
validateMaxLength,
|
||||||
|
validateMinLength,
|
||||||
|
validateRequired,
|
||||||
|
type FieldErrors,
|
||||||
|
type ValidationResult,
|
||||||
|
} from "./primitives";
|
||||||
|
|
||||||
|
export type Ruolo = (typeof UTENTE_CONSTRAINTS.ruolo.allowedValues)[number];
|
||||||
|
|
||||||
|
export interface LoginInput {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UtenteCreateInput {
|
||||||
|
nome: string;
|
||||||
|
cognome: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
ruolo: Ruolo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UtenteUpdateInput = Partial<UtenteCreateInput>;
|
||||||
|
|
||||||
|
export function normalizeLoginInput(raw: Record<string, unknown>): LoginInput {
|
||||||
|
return {
|
||||||
|
email: normalizeEmail(raw.email),
|
||||||
|
password: raw.password?.toString() ?? "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateLoginInput(
|
||||||
|
raw: Record<string, unknown>
|
||||||
|
): ValidationResult<LoginInput> {
|
||||||
|
const data = normalizeLoginInput(raw);
|
||||||
|
const errors: FieldErrors = {};
|
||||||
|
|
||||||
|
validateRequired(errors, "email", data.email, "Email obbligatoria");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"email",
|
||||||
|
data.email,
|
||||||
|
UTENTE_CONSTRAINTS.email.maxLength,
|
||||||
|
"Email"
|
||||||
|
);
|
||||||
|
validateEmailFormat(errors, "email", data.email);
|
||||||
|
validateRequired(errors, "password", data.password, "Password obbligatoria");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"password",
|
||||||
|
data.password,
|
||||||
|
UTENTE_CONSTRAINTS.password.maxLength,
|
||||||
|
"Password"
|
||||||
|
);
|
||||||
|
|
||||||
|
return buildResult(data, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateUtenteProfileFields(
|
||||||
|
data: UtenteCreateInput,
|
||||||
|
errors: FieldErrors
|
||||||
|
): void {
|
||||||
|
validateRequired(errors, "nome", data.nome, "Nome obbligatorio");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"nome",
|
||||||
|
data.nome,
|
||||||
|
UTENTE_CONSTRAINTS.nome.maxLength,
|
||||||
|
"Nome"
|
||||||
|
);
|
||||||
|
validateRequired(errors, "cognome", data.cognome, "Cognome obbligatorio");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"cognome",
|
||||||
|
data.cognome,
|
||||||
|
UTENTE_CONSTRAINTS.cognome.maxLength,
|
||||||
|
"Cognome"
|
||||||
|
);
|
||||||
|
validateRequired(errors, "email", data.email, "Email obbligatoria");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"email",
|
||||||
|
data.email,
|
||||||
|
UTENTE_CONSTRAINTS.email.maxLength,
|
||||||
|
"Email"
|
||||||
|
);
|
||||||
|
validateEmailFormat(errors, "email", data.email);
|
||||||
|
|
||||||
|
validateRequired(errors, "password", data.password, "Password obbligatoria");
|
||||||
|
validateMinLength(
|
||||||
|
errors,
|
||||||
|
"password",
|
||||||
|
data.password,
|
||||||
|
UTENTE_CONSTRAINTS.password.minLength,
|
||||||
|
"Password"
|
||||||
|
);
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"password",
|
||||||
|
data.password,
|
||||||
|
UTENTE_CONSTRAINTS.password.maxLength,
|
||||||
|
"Password"
|
||||||
|
);
|
||||||
|
|
||||||
|
validateRequired(errors, "ruolo", data.ruolo, "Ruolo obbligatorio");
|
||||||
|
validateEnum(
|
||||||
|
errors,
|
||||||
|
"ruolo",
|
||||||
|
data.ruolo,
|
||||||
|
UTENTE_CONSTRAINTS.ruolo.allowedValues,
|
||||||
|
"Ruolo"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateUtenteCreate(
|
||||||
|
raw: Record<string, unknown>
|
||||||
|
): ValidationResult<UtenteCreateInput> {
|
||||||
|
const data: UtenteCreateInput = {
|
||||||
|
nome: normalizeString(raw.nome),
|
||||||
|
cognome: normalizeString(raw.cognome),
|
||||||
|
email: normalizeEmail(raw.email),
|
||||||
|
password: raw.password?.toString() ?? "",
|
||||||
|
ruolo: normalizeString(raw.ruolo) as Ruolo,
|
||||||
|
};
|
||||||
|
const errors: FieldErrors = {};
|
||||||
|
|
||||||
|
validateUtenteProfileFields(data, errors);
|
||||||
|
|
||||||
|
return buildResult(data, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateUtenteUpdate(
|
||||||
|
raw: Record<string, unknown>
|
||||||
|
): ValidationResult<UtenteUpdateInput> {
|
||||||
|
const errors: FieldErrors = {};
|
||||||
|
const data: UtenteUpdateInput = {};
|
||||||
|
|
||||||
|
if ("nome" in raw) {
|
||||||
|
data.nome = normalizeString(raw.nome);
|
||||||
|
validateRequired(errors, "nome", data.nome, "Nome obbligatorio");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"nome",
|
||||||
|
data.nome,
|
||||||
|
UTENTE_CONSTRAINTS.nome.maxLength,
|
||||||
|
"Nome"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("cognome" in raw) {
|
||||||
|
data.cognome = normalizeString(raw.cognome);
|
||||||
|
validateRequired(errors, "cognome", data.cognome, "Cognome obbligatorio");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"cognome",
|
||||||
|
data.cognome,
|
||||||
|
UTENTE_CONSTRAINTS.cognome.maxLength,
|
||||||
|
"Cognome"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("email" in raw) {
|
||||||
|
data.email = normalizeEmail(raw.email);
|
||||||
|
validateRequired(errors, "email", data.email, "Email obbligatoria");
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"email",
|
||||||
|
data.email,
|
||||||
|
UTENTE_CONSTRAINTS.email.maxLength,
|
||||||
|
"Email"
|
||||||
|
);
|
||||||
|
validateEmailFormat(errors, "email", data.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("password" in raw) {
|
||||||
|
const password = raw.password?.toString() ?? "";
|
||||||
|
if (password) {
|
||||||
|
data.password = password;
|
||||||
|
validateMinLength(
|
||||||
|
errors,
|
||||||
|
"password",
|
||||||
|
password,
|
||||||
|
UTENTE_CONSTRAINTS.password.minLength,
|
||||||
|
"Password"
|
||||||
|
);
|
||||||
|
validateMaxLength(
|
||||||
|
errors,
|
||||||
|
"password",
|
||||||
|
password,
|
||||||
|
UTENTE_CONSTRAINTS.password.maxLength,
|
||||||
|
"Password"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("ruolo" in raw) {
|
||||||
|
data.ruolo = normalizeString(raw.ruolo) as Ruolo;
|
||||||
|
validateRequired(errors, "ruolo", data.ruolo, "Ruolo obbligatorio");
|
||||||
|
validateEnum(
|
||||||
|
errors,
|
||||||
|
"ruolo",
|
||||||
|
data.ruolo,
|
||||||
|
UTENTE_CONSTRAINTS.ruolo.allowedValues,
|
||||||
|
"Ruolo"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(data).length === 0) {
|
||||||
|
errors._form = "Nessun campo da aggiornare";
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildResult(data, errors);
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue