Add scouthub-attivit-be

This commit is contained in:
Lorenzo Sanesi
2026-07-23 19:34:00 +02:00
parent 904cd2ee9b
commit 2ee7d402b6
55 changed files with 9783 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
import express from 'express';
import cors from 'cors';
import { env } from './config/env';
import { authenticate } from './middlewares/authenticate';
import { errorHandler } from './middlewares/errorHandler';
import { attivitaPublicRouter } from './modules/attivita/attivita.router.public';
import { attivitaPrivateRouter } from './modules/attivita/attivita.router.private';
import { autocompleteRouter } from './modules/autocomplete/autocomplete.router';
export const app = express();
// strict: false perché gli endpoint di autocomplete (vedi extractKeyword)
// accettano un body JSON che è una stringa "nuda" (es. `"gioco"`), non solo
// oggetti/array: con lo strict mode di default body-parser la rifiuterebbe.
app.use(express.json({ strict: false }));
app.use(cors({ origin: env.corsOrigin }));
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
app.use('/public/attivita', attivitaPublicRouter);
app.use('/private/attivita', authenticate, attivitaPrivateRouter);
app.use('/public/autocomplete', autocompleteRouter);
app.use((req, res) => {
res.status(404).json({ message: 'not found' });
});
app.use(errorHandler);
+21
View File
@@ -0,0 +1,21 @@
import dotenv from 'dotenv';
dotenv.config();
function requireEnv(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`Variabile d'ambiente mancante: ${name}`);
}
return value;
}
export const env = {
port: Number(process.env.PORT) || 8080,
corsOrigin: process.env.CORS_ORIGIN || 'http://localhost:4200',
databaseUrl: requireEnv('DATABASE_URL'),
keycloak: {
baseUrl: requireEnv('KEYCLOAK_BASE_URL'),
realm: requireEnv('KEYCLOAK_REALM'),
},
};
+12
View File
@@ -0,0 +1,12 @@
import { PrismaClient } from '@prisma/client';
declare global {
// eslint-disable-next-line no-var
var __prisma: PrismaClient | undefined;
}
export const prisma = global.__prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') {
global.__prisma = prisma;
}
+9
View File
@@ -0,0 +1,9 @@
export class HttpError extends Error {
statusCode: number;
constructor(statusCode: number, message: string) {
super(message);
this.statusCode = statusCode;
this.name = 'HttpError';
}
}
@@ -0,0 +1,6 @@
export interface AuthContext {
userId: string;
email: string | null;
name: string;
roles: string[];
}
@@ -0,0 +1,67 @@
import { Request, Response, NextFunction } from 'express';
import jwt, { JwtPayload } from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
import { env } from '../config/env';
import { AuthContext } from './auth.types';
const client = jwksClient({
jwksUri: `${env.keycloak.baseUrl}/realms/${env.keycloak.realm}/protocol/openid-connect/certs`,
cache: true,
rateLimit: true,
});
interface KeycloakTokenPayload extends JwtPayload {
sub: string;
email?: string;
name?: string;
preferred_username?: string;
realm_access?: { roles?: string[] };
}
function extractBearerToken(req: Request): string | null {
const header = req.headers.authorization;
if (!header) {
return null;
}
const [scheme, token] = header.split(' ');
if (scheme !== 'Bearer' || !token) {
return null;
}
return token;
}
function buildAuthContext(payload: KeycloakTokenPayload): AuthContext {
return {
userId: payload.sub,
email: payload.email ?? null,
name: payload.name ?? payload.preferred_username ?? payload.email ?? payload.sub,
roles: payload.realm_access?.roles ?? [],
};
}
export async function authenticate(req: Request, res: Response, next: NextFunction): Promise<void> {
const token = extractBearerToken(req);
if (!token) {
res.status(401).json({ message: 'Token mancante' });
return;
}
try {
const decoded = jwt.decode(token, { complete: true });
if (!decoded || !decoded.header.kid) {
throw new Error("Header del token privo di 'kid'");
}
const signingKey = await client.getSigningKey(decoded.header.kid);
const payload = jwt.verify(token, signingKey.getPublicKey(), { algorithms: ['RS256'] });
if (typeof payload === 'string') {
throw new Error('Payload del token non valido');
}
req.auth = buildAuthContext(payload as KeycloakTokenPayload);
next();
} catch {
res.status(401).json({ message: 'Token non valido' });
}
}
@@ -0,0 +1,10 @@
import { Request, Response, NextFunction } from 'express';
export function errorHandler(err: unknown, req: Request, res: Response, next: NextFunction): void {
const hasStatusCode =
typeof err === 'object' && err !== null && 'statusCode' in err && typeof (err as { statusCode?: unknown }).statusCode === 'number';
const statusCode = hasStatusCode ? (err as { statusCode: number }).statusCode : 500;
const message = err instanceof Error && err.message ? err.message : 'Errore interno del server';
res.status(statusCode).json({ message });
}
@@ -0,0 +1,51 @@
import { Request, Response, NextFunction, Router } from 'express';
import { HttpError } from '../../errors';
import { attivitaSaveSchema } from '../../types/validation';
import * as attivitaService from './attivita.service';
function asyncHandler(
handler: (req: Request, res: Response, next: NextFunction) => Promise<void>,
) {
return (req: Request, res: Response, next: NextFunction): void => {
handler(req, res, next).catch(next);
};
}
export const attivitaPrivateRouter = Router();
attivitaPrivateRouter.get(
'/get/lista/my',
asyncHandler(async (req, res) => {
const lista = await attivitaService.getListMy(req.auth!.userId);
res.status(200).json(lista);
}),
);
attivitaPrivateRouter.get(
'/change/stato/:idAttivita/:idStato',
asyncHandler(async (req, res, next) => {
const idAttivita = Number(req.params.idAttivita);
if (!Number.isInteger(idAttivita)) {
next(new HttpError(400, "l'idAttivita deve essere un numero intero"));
return;
}
const idStato = req.params.idStato;
const stato = await attivitaService.changeStato(idAttivita, idStato, req.auth!);
res.status(200).json(stato);
}),
);
attivitaPrivateRouter.post(
'/save',
asyncHandler(async (req, res, next) => {
const parsed = attivitaSaveSchema.safeParse(req.body);
if (!parsed.success) {
next(new HttpError(400, JSON.stringify(parsed.error.flatten())));
return;
}
await attivitaService.save(parsed.data, req.auth!);
res.status(200).end();
}),
);
@@ -0,0 +1,63 @@
import { Request, Response, NextFunction, Router } from 'express';
import { z } from 'zod';
import { HttpError } from '../../errors';
import * as attivitaService from './attivita.service';
const searchObjectSchema = z.array(
z.object({
id: z.number().nullable(),
nome: z.string().nullable(),
gruppo: z.string(),
}),
);
function asyncHandler(
handler: (req: Request, res: Response, next: NextFunction) => Promise<void>,
) {
return (req: Request, res: Response, next: NextFunction): void => {
handler(req, res, next).catch(next);
};
}
export const attivitaPublicRouter = Router();
attivitaPublicRouter.get(
'/get/lista/home',
asyncHandler(async (req, res) => {
const lista = await attivitaService.getListaHome();
res.status(200).json(lista);
}),
);
attivitaPublicRouter.post(
'/get/lista/search',
asyncHandler(async (req, res, next) => {
const parsed = searchObjectSchema.safeParse(req.body);
if (!parsed.success) {
next(new HttpError(400, 'body non valido: atteso un array di SearchObjectDto'));
return;
}
const lista = await attivitaService.getListaSearch(parsed.data);
res.status(200).json(lista);
}),
);
attivitaPublicRouter.get(
'/get/one/:id',
asyncHandler(async (req, res, next) => {
const id = Number(req.params.id);
if (!Number.isInteger(id)) {
next(new HttpError(400, "l'id deve essere un numero intero"));
return;
}
const attivita = await attivitaService.getOne(id);
if (!attivita) {
next(new HttpError(404, 'attività non trovata'));
return;
}
res.status(200).json(attivita);
}),
);
@@ -0,0 +1,560 @@
import { Prisma, PrismaClient } from '@prisma/client';
import { prisma } from '../../db/prisma';
import { HttpError } from '../../errors';
import { AuthContext } from '../../middlewares/auth.types';
import {
AttivitaDto,
BrancaDto,
CategoriaDto,
MaterialeDto,
ParagrafoDto,
PeriodoAnnoDto,
SearchObjectDto,
TipologicaDto,
} from '../../types/dto';
import { AttivitaSaveInput } from '../../types/validation';
const attivitaInclude = {
stato: true,
brancaLinks: { include: { branca: true } },
categoriaLinks: { include: { categoria: { include: { tipo: true } } } },
materialeLinks: { include: { materiale: true } },
periodoAnnoLinks: { include: { periodoAnno: true } },
paragrafi: { include: { tipo: true } },
} satisfies Prisma.AttivitaInclude;
type AttivitaWithRelations = Prisma.AttivitaGetPayload<{ include: typeof attivitaInclude }>;
function toTipologicaDto(entity: { id: string; nome: string }): TipologicaDto {
return { id: entity.id, nome: entity.nome };
}
function toAttivitaDto(entity: AttivitaWithRelations): AttivitaDto {
const brancaList: BrancaDto[] = entity.brancaLinks
.filter((link) => !link.cancellato)
.map((link) => ({
id: link.branca.id,
nome: link.branca.nome,
inizioEta: link.branca.inizioEta,
fineEta: link.branca.fineEta,
colore: link.branca.colore,
cancellato: false,
dataCreazione: link.branca.dataCreazione,
dataModifica: link.branca.dataModifica,
utenteModifica: link.branca.utenteModifica,
}));
const categoriaList: CategoriaDto[] = entity.categoriaLinks
.filter((link) => !link.cancellato)
.map((link) => ({
id: link.categoria.id,
nome: link.categoria.nome,
padre: link.categoria.padreId,
tipo: toTipologicaDto(link.categoria.tipo),
cancellato: false,
dataCreazione: link.categoria.dataCreazione,
dataModifica: link.categoria.dataModifica,
utenteModifica: link.categoria.utenteModifica,
}));
const materialeList: MaterialeDto[] = entity.materialeLinks
.filter((link) => !link.cancellato)
.map((link) => ({
id: link.materiale.id,
nome: link.materiale.nome,
proprieta: link.proprieta,
cancellato: false,
dataCreazione: link.materiale.dataCreazione,
dataModifica: link.materiale.dataModifica,
utenteModifica: link.materiale.utenteModifica,
}));
const periodoAnnoList: PeriodoAnnoDto[] = entity.periodoAnnoLinks
.filter((link) => !link.cancellato)
.map((link) => ({
id: link.periodoAnno.id,
nome: link.periodoAnno.nome,
inizioMese: link.periodoAnno.inizioMese,
fineMese: link.periodoAnno.fineMese,
cancellato: false,
dataCreazione: link.periodoAnno.dataCreazione,
dataModifica: link.periodoAnno.dataModifica,
utenteModifica: link.periodoAnno.utenteModifica,
}));
const paragrafoList: ParagrafoDto[] = [...entity.paragrafi]
.sort((a, b) => a.ordine - b.ordine)
.map((paragrafo) => ({
id: paragrafo.id,
attivitaId: paragrafo.attivitaId,
corpo: paragrafo.corpo,
autore: paragrafo.autore,
tipo: toTipologicaDto(paragrafo.tipo),
ordine: paragrafo.ordine,
dataCreazione: paragrafo.dataCreazione,
dataModifica: paragrafo.dataModifica,
utenteModifica: paragrafo.utenteModifica,
}));
return {
id: entity.id,
nome: entity.nome,
autore: entity.autore,
padre: entity.padreId,
stato: toTipologicaDto(entity.stato),
brancaList,
categoriaList,
materialeList,
paragrafoList,
periodoAnnoList,
dataCreazione: entity.dataCreazione,
dataModifica: entity.dataModifica,
utenteModifica: entity.utenteModifica,
};
}
export async function getListaHome(): Promise<AttivitaDto[]> {
const entities = await prisma.attivita.findMany({
where: { statoId: 'PU' },
include: attivitaInclude,
orderBy: { dataModifica: 'desc' },
});
return entities.map(toAttivitaDto);
}
export async function getOne(id: number): Promise<AttivitaDto | null> {
const entity = await prisma.attivita.findUnique({
where: { id },
include: attivitaInclude,
});
return entity ? toAttivitaDto(entity) : null;
}
export async function getListMy(autoreId: string): Promise<AttivitaDto[]> {
const entities = await prisma.attivita.findMany({
where: { autoreId },
include: attivitaInclude,
orderBy: { dataModifica: 'desc' },
});
return entities.map(toAttivitaDto);
}
export async function getListaSearch(dtoList: SearchObjectDto[]): Promise<AttivitaDto[]> {
const brancaIds = dtoList.filter((d) => d.gruppo === 'branca').map((d) => d.id as number);
const categoriaIds = dtoList.filter((d) => d.gruppo === 'categoria').map((d) => d.id as number);
const materialeIds = dtoList.filter((d) => d.gruppo === 'materiale').map((d) => d.id as number);
const periodoAnnoIds = dtoList
.filter((d) => d.gruppo === 'periodoAnno')
.map((d) => d.id as number);
const testoList = dtoList
.filter((d) => d.gruppo === 'testo')
.map((d) => d.nome)
.filter((nome): nome is string => !!nome);
const and: Prisma.AttivitaWhereInput[] = [];
if (brancaIds.length > 0) {
and.push({
brancaLinks: { some: { brancaId: { in: brancaIds }, cancellato: false } },
});
}
if (categoriaIds.length > 0) {
and.push({
categoriaLinks: { some: { categoriaId: { in: categoriaIds }, cancellato: false } },
});
}
if (materialeIds.length > 0) {
and.push({
materialeLinks: { some: { materialeId: { in: materialeIds }, cancellato: false } },
});
}
if (periodoAnnoIds.length > 0) {
and.push({
periodoAnnoLinks: { some: { periodoAnnoId: { in: periodoAnnoIds }, cancellato: false } },
});
}
if (testoList.length > 0) {
and.push({
OR: testoList.flatMap((testo) => [
{ nome: { contains: testo, mode: 'insensitive' } },
{ paragrafi: { some: { corpo: { contains: testo, mode: 'insensitive' } } } },
]),
});
}
const entities = await prisma.attivita.findMany({
where: and.length > 0 ? { AND: and } : {},
include: attivitaInclude,
orderBy: { dataModifica: 'desc' },
});
return entities.map(toAttivitaDto);
}
export async function changeStato(
idAttivita: number,
idStato: string,
auth: AuthContext,
): Promise<TipologicaDto> {
const existing = await prisma.attivita.findUnique({ where: { id: idAttivita } });
if (!existing) {
throw new HttpError(404, 'attività non trovata');
}
if (existing.autoreId !== auth.userId) {
throw new HttpError(403, 'non sei autore di questa attività');
}
const updated = await prisma.attivita.update({
where: { id: idAttivita },
data: {
statoId: idStato,
utenteModifica: auth.name,
},
include: { stato: true },
});
return toTipologicaDto(updated.stato);
}
type Tx = Omit<PrismaClient, '$connect' | '$disconnect' | '$on' | '$transaction' | '$use' | '$extends'>;
async function upsertBranca(
tx: Tx,
attivitaId: number,
input: AttivitaSaveInput['brancaList'][number],
auth: AuthContext,
): Promise<number> {
let brancaId = input.id;
if (!brancaId) {
const found = await tx.branca.findFirst({
where: { nome: { equals: input.nome, mode: 'insensitive' } },
});
if (found) {
brancaId = found.id;
} else {
const created = await tx.branca.create({
data: {
nome: input.nome,
utenteModifica: auth.name,
},
});
brancaId = created.id;
}
}
const existingLink = await tx.brancaAttivita.findUnique({
where: { attivitaId_brancaId: { attivitaId, brancaId } },
});
if (existingLink) {
await tx.brancaAttivita.update({
where: { attivitaId_brancaId: { attivitaId, brancaId } },
data: {
cancellato: false,
utenteModifica: auth.name,
},
});
} else {
await tx.brancaAttivita.create({
data: {
attivitaId,
brancaId,
cancellato: false,
utenteModifica: auth.name,
},
});
}
return brancaId;
}
async function upsertCategoria(
tx: Tx,
attivitaId: number,
input: AttivitaSaveInput['categoriaList'][number],
auth: AuthContext,
): Promise<number> {
let categoriaId = input.id;
if (!categoriaId) {
const found = await tx.categoria.findFirst({
where: { nome: { equals: input.nome, mode: 'insensitive' } },
});
if (found) {
categoriaId = found.id;
} else {
const created = await tx.categoria.create({
data: {
nome: input.nome,
tipoId: 'A',
utenteModifica: auth.name,
},
});
categoriaId = created.id;
}
}
const existingLink = await tx.categoriaAttivita.findUnique({
where: { attivitaId_categoriaId: { attivitaId, categoriaId } },
});
if (existingLink) {
await tx.categoriaAttivita.update({
where: { attivitaId_categoriaId: { attivitaId, categoriaId } },
data: {
cancellato: false,
utenteModifica: auth.name,
},
});
} else {
await tx.categoriaAttivita.create({
data: {
attivitaId,
categoriaId,
cancellato: false,
utenteModifica: auth.name,
},
});
}
return categoriaId;
}
async function upsertMateriale(
tx: Tx,
attivitaId: number,
input: AttivitaSaveInput['materialeList'][number],
auth: AuthContext,
): Promise<number> {
let materialeId = input.id;
if (!materialeId) {
const found = await tx.materiale.findFirst({
where: { nome: { equals: input.nome, mode: 'insensitive' } },
});
if (found) {
materialeId = found.id;
} else {
const created = await tx.materiale.create({
data: {
nome: input.nome,
utenteModifica: auth.name,
},
});
materialeId = created.id;
}
}
const proprieta = (input.proprieta ?? Prisma.JsonNull) as Prisma.InputJsonValue;
const existingLink = await tx.materialeAttivita.findUnique({
where: { attivitaId_materialeId: { attivitaId, materialeId } },
});
if (existingLink) {
await tx.materialeAttivita.update({
where: { attivitaId_materialeId: { attivitaId, materialeId } },
data: {
cancellato: false,
proprieta,
utenteModifica: auth.name,
},
});
} else {
await tx.materialeAttivita.create({
data: {
attivitaId,
materialeId,
cancellato: false,
proprieta,
utenteModifica: auth.name,
},
});
}
return materialeId;
}
async function upsertPeriodoAnno(
tx: Tx,
attivitaId: number,
input: AttivitaSaveInput['periodoAnnoList'][number],
auth: AuthContext,
): Promise<number> {
let periodoAnnoId = input.id;
if (!periodoAnnoId) {
const found = await tx.periodoAnno.findFirst({
where: { nome: { equals: input.nome, mode: 'insensitive' } },
});
if (found) {
periodoAnnoId = found.id;
} else {
const created = await tx.periodoAnno.create({
data: {
nome: input.nome,
utenteModifica: auth.name,
},
});
periodoAnnoId = created.id;
}
}
const existingLink = await tx.periodoAnnoAttivita.findUnique({
where: { attivitaId_periodoAnnoId: { attivitaId, periodoAnnoId } },
});
if (existingLink) {
await tx.periodoAnnoAttivita.update({
where: { attivitaId_periodoAnnoId: { attivitaId, periodoAnnoId } },
data: {
cancellato: false,
utenteModifica: auth.name,
},
});
} else {
await tx.periodoAnnoAttivita.create({
data: {
attivitaId,
periodoAnnoId,
cancellato: false,
utenteModifica: auth.name,
},
});
}
return periodoAnnoId;
}
export async function save(dto: AttivitaSaveInput, auth: AuthContext): Promise<void> {
await prisma.$transaction(async (tx) => {
let attivitaId: number;
if (dto.id) {
const existing = await tx.attivita.findUnique({ where: { id: dto.id } });
if (!existing) {
throw new HttpError(404, 'attività non trovata');
}
if (existing.autoreId !== auth.userId) {
throw new HttpError(403, 'non sei autore di questa attività');
}
await tx.attivita.update({
where: { id: dto.id },
data: {
nome: dto.nome,
statoId: dto.stato.id,
utenteModifica: auth.name,
},
});
attivitaId = dto.id;
} else {
const created = await tx.attivita.create({
data: {
nome: dto.nome,
autore: auth.name,
autoreId: auth.userId,
statoId: dto.stato.id,
utenteModifica: auth.name,
},
});
attivitaId = created.id;
}
const brancaIds = new Set<number>();
for (const branca of dto.brancaList) {
brancaIds.add(await upsertBranca(tx, attivitaId, branca, auth));
}
await tx.brancaAttivita.updateMany({
where: { attivitaId, brancaId: { notIn: [...brancaIds] }, cancellato: false },
data: {
cancellato: true,
utenteModifica: auth.name,
},
});
const categoriaIds = new Set<number>();
for (const categoria of dto.categoriaList) {
categoriaIds.add(await upsertCategoria(tx, attivitaId, categoria, auth));
}
await tx.categoriaAttivita.updateMany({
where: { attivitaId, categoriaId: { notIn: [...categoriaIds] }, cancellato: false },
data: {
cancellato: true,
utenteModifica: auth.name,
},
});
const materialeIds = new Set<number>();
for (const materiale of dto.materialeList) {
materialeIds.add(await upsertMateriale(tx, attivitaId, materiale, auth));
}
await tx.materialeAttivita.updateMany({
where: { attivitaId, materialeId: { notIn: [...materialeIds] }, cancellato: false },
data: {
cancellato: true,
utenteModifica: auth.name,
},
});
const periodoAnnoIds = new Set<number>();
for (const periodoAnno of dto.periodoAnnoList) {
periodoAnnoIds.add(await upsertPeriodoAnno(tx, attivitaId, periodoAnno, auth));
}
await tx.periodoAnnoAttivita.updateMany({
where: { attivitaId, periodoAnnoId: { notIn: [...periodoAnnoIds] }, cancellato: false },
data: {
cancellato: true,
utenteModifica: auth.name,
},
});
const paragrafoIds = new Set<number>();
for (const paragrafo of dto.paragrafoList) {
// Il paragrafo non ha un autore proprio nell'API: eredita quello dell'attivita'
// se non esplicitamente indicato nel payload.
const paragrafoAutore = paragrafo.autore ?? auth.name;
if (paragrafo.id) {
await tx.paragrafo.update({
where: { id: paragrafo.id },
data: {
corpo: paragrafo.corpo,
autore: paragrafoAutore,
tipoId: paragrafo.tipo.id,
ordine: paragrafo.ordine,
utenteModifica: auth.name,
},
});
paragrafoIds.add(paragrafo.id);
} else {
const created = await tx.paragrafo.create({
data: {
attivitaId,
corpo: paragrafo.corpo,
autore: paragrafoAutore,
tipoId: paragrafo.tipo.id,
ordine: paragrafo.ordine,
utenteModifica: auth.name,
},
});
paragrafoIds.add(created.id);
}
}
await tx.paragrafo.deleteMany({
where: { attivitaId, id: { notIn: [...paragrafoIds] } },
});
});
}
@@ -0,0 +1,56 @@
import { Request, Response, NextFunction, Router } from 'express';
import * as autocompleteService from './autocomplete.service';
function asyncHandler(
handler: (req: Request, res: Response, next: NextFunction) => Promise<void>,
) {
return (req: Request, res: Response, next: NextFunction): void => {
handler(req, res, next).catch(next);
};
}
function extractKeyword(body: unknown): string | undefined {
return typeof body === 'string' ? body : undefined;
}
export const autocompleteRouter = Router();
autocompleteRouter.post(
'/get/search',
asyncHandler(async (req, res) => {
const groups = await autocompleteService.getSearch(extractKeyword(req.body));
res.status(200).json(groups);
}),
);
autocompleteRouter.post(
'/get/branca',
asyncHandler(async (req, res) => {
const lista = await autocompleteService.getBranca(extractKeyword(req.body));
res.status(200).json(lista);
}),
);
autocompleteRouter.post(
'/get/categoria',
asyncHandler(async (req, res) => {
const lista = await autocompleteService.getCategoria(extractKeyword(req.body));
res.status(200).json(lista);
}),
);
autocompleteRouter.post(
'/get/materiale',
asyncHandler(async (req, res) => {
const lista = await autocompleteService.getMateriale(extractKeyword(req.body));
res.status(200).json(lista);
}),
);
autocompleteRouter.post(
'/get/periodoAnno',
asyncHandler(async (req, res) => {
const lista = await autocompleteService.getPeriodoAnno(extractKeyword(req.body));
res.status(200).json(lista);
}),
);
@@ -0,0 +1,62 @@
import { prisma } from '../../db/prisma';
import { SearchGroupDto, SearchObjectDto } from '../../types/dto';
export async function getBranca(keyword?: string | null): Promise<SearchObjectDto[]> {
const entities = await prisma.branca.findMany({
where: { nome: { contains: keyword ?? '', mode: 'insensitive' } },
});
return entities.map((entity) => ({ id: entity.id, nome: entity.nome, gruppo: 'branca' }));
}
export async function getCategoria(keyword?: string | null): Promise<SearchObjectDto[]> {
const entities = await prisma.categoria.findMany({
where: { nome: { contains: keyword ?? '', mode: 'insensitive' } },
});
return entities.map((entity) => ({ id: entity.id, nome: entity.nome, gruppo: 'categoria' }));
}
export async function getMateriale(keyword?: string | null): Promise<SearchObjectDto[]> {
const entities = await prisma.materiale.findMany({
where: { nome: { contains: keyword ?? '', mode: 'insensitive' } },
});
return entities.map((entity) => ({ id: entity.id, nome: entity.nome, gruppo: 'materiale' }));
}
export async function getPeriodoAnno(keyword?: string | null): Promise<SearchObjectDto[]> {
const entities = await prisma.periodoAnno.findMany({
where: { nome: { contains: keyword ?? '', mode: 'insensitive' } },
});
return entities.map((entity) => ({ id: entity.id, nome: entity.nome, gruppo: 'periodoAnno' }));
}
export async function getSearch(keyword?: string | null): Promise<SearchGroupDto[]> {
const groups: SearchGroupDto[] = [
{ label: 'Testo', objectsList: [{ id: null, nome: keyword ?? null, gruppo: 'testo' }] },
];
const brancaList = await getBranca(keyword);
if (brancaList.length > 0) {
groups.push({ label: 'Branca', objectsList: brancaList });
}
const categoriaList = await getCategoria(keyword);
if (categoriaList.length > 0) {
groups.push({ label: 'Categoria', objectsList: categoriaList });
}
const materialeList = await getMateriale(keyword);
if (materialeList.length > 0) {
groups.push({ label: 'Materiale', objectsList: materialeList });
}
const periodoAnnoList = await getPeriodoAnno(keyword);
if (periodoAnnoList.length > 0) {
groups.push({ label: 'Periodo anno', objectsList: periodoAnnoList });
}
return groups;
}
+6
View File
@@ -0,0 +1,6 @@
import { app } from './app';
import { env } from './config/env';
app.listen(env.port, () => {
console.log(`Server avviato su porta ${env.port} (CORS origin: ${env.corsOrigin})`);
});
+76
View File
@@ -0,0 +1,76 @@
export interface TipologicaDto {
id: string;
nome: string;
}
export interface BaseDto {
dataCreazione: Date;
dataModifica: Date;
utenteModifica: string;
}
export interface BrancaDto extends BaseDto {
id: number;
nome: string;
inizioEta: number | null;
fineEta: number | null;
colore: string | null;
cancellato: boolean;
}
export interface CategoriaDto extends BaseDto {
id: number;
nome: string;
padre: number | null;
tipo: TipologicaDto;
cancellato: boolean;
}
export interface MaterialeDto extends BaseDto {
id: number;
nome: string;
proprieta: unknown;
categoriaList?: CategoriaDto[];
cancellato: boolean;
}
export interface ParagrafoDto extends BaseDto {
id: number;
attivitaId: number;
corpo: string;
autore: string;
tipo: TipologicaDto;
ordine: number;
}
export interface PeriodoAnnoDto extends BaseDto {
id: number;
nome: string;
inizioMese: number | null;
fineMese: number | null;
cancellato: boolean;
}
export interface AttivitaDto extends BaseDto {
id: number | null;
nome: string;
autore: string;
padre: number | null;
stato: TipologicaDto;
brancaList: BrancaDto[];
categoriaList: CategoriaDto[];
materialeList: MaterialeDto[];
paragrafoList: ParagrafoDto[];
periodoAnnoList: PeriodoAnnoDto[];
}
export interface SearchObjectDto {
id: number | null;
nome: string | null;
gruppo: string;
}
export interface SearchGroupDto {
label: string;
objectsList: SearchObjectDto[];
}
+11
View File
@@ -0,0 +1,11 @@
import { AuthContext } from '../middlewares/auth.types';
declare global {
namespace Express {
interface Request {
auth?: AuthContext;
}
}
}
export {};
@@ -0,0 +1,58 @@
import { z } from 'zod';
export const tipologicaSchema = z.object({
id: z.string(),
nome: z.string(),
});
export const paragrafoInputSchema = z.object({
id: z.number().optional(),
corpo: z.string().min(1).max(500),
// Un paragrafo non ha un autore proprio nell'API/frontend: eredita quello
// dell'attivita' se non esplicitamente indicato (vedi attivita.service.ts).
autore: z.string().min(1).optional(),
tipo: z.object({
id: z.string(),
}),
ordine: z.number().int(),
});
export const brancaInputSchema = z.object({
id: z.number().optional(),
nome: z.string().min(1),
});
export const categoriaInputSchema = z.object({
id: z.number().optional(),
nome: z.string().min(1),
});
export const materialeInputSchema = z.object({
id: z.number().optional(),
nome: z.string().min(1),
proprieta: z.unknown().optional(),
});
export const periodoAnnoInputSchema = z.object({
id: z.number().optional(),
nome: z.string().min(1),
});
export const attivitaSaveSchema = z.object({
id: z.number().optional(),
nome: z.string().min(1),
stato: tipologicaSchema,
brancaList: z.array(brancaInputSchema),
categoriaList: z.array(categoriaInputSchema),
materialeList: z.array(materialeInputSchema),
periodoAnnoList: z.array(periodoAnnoInputSchema),
paragrafoList: z.array(paragrafoInputSchema),
});
export type TipologicaInput = z.infer<typeof tipologicaSchema>;
export type ParagrafoInput = z.infer<typeof paragrafoInputSchema>;
export type BrancaInput = z.infer<typeof brancaInputSchema>;
export type CategoriaInput = z.infer<typeof categoriaInputSchema>;
export type MaterialeInput = z.infer<typeof materialeInputSchema>;
export type PeriodoAnnoInput = z.infer<typeof periodoAnnoInputSchema>;
export type AttivitaSaveInput = z.infer<typeof attivitaSaveSchema>;