Add scouthub-attivit-be
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
coverage
|
||||||
|
.git
|
||||||
|
.idea
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
PORT=8080
|
||||||
|
CORS_ORIGIN=http://localhost:4200
|
||||||
|
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/scouthub?schema=public
|
||||||
|
|
||||||
|
# Keycloak (realm condiviso con scouthub-home-be/-fe, vedi keycloak/realm-export.json
|
||||||
|
# alla radice del monorepo): usato solo per verificare i token, nessun client
|
||||||
|
# id/secret necessario qui.
|
||||||
|
KEYCLOAK_BASE_URL=http://localhost:8081
|
||||||
|
KEYCLOAK_REALM=scouthub
|
||||||
|
|
||||||
|
# Database dedicato ai test automatici (Jest): tenuto separato dal DB di sviluppo
|
||||||
|
# perche' la suite lo pulisce (TRUNCATE) tra un test e l'altro.
|
||||||
|
DATABASE_URL_TEST=postgresql://postgres:postgres@localhost:5432/scouthub_test?schema=public
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
/attivita-be.iml
|
||||||
|
/.idea/
|
||||||
|
/bash.exe.stackdump
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
-- **************************************************** stato_attivita
|
||||||
|
|
||||||
|
create table stato_attivita
|
||||||
|
(
|
||||||
|
id varchar(2) not null primary key,
|
||||||
|
nome varchar(250) not null,
|
||||||
|
data_creazione datetime not null,
|
||||||
|
data_modifica datetime not null,
|
||||||
|
utente_modifica varchar(50) not null
|
||||||
|
);
|
||||||
|
|
||||||
|
-- **************************************************** tipo_categoria
|
||||||
|
|
||||||
|
create table tipo_categoria
|
||||||
|
(
|
||||||
|
id varchar(2) not null primary key,
|
||||||
|
nome varchar(250) not null,
|
||||||
|
data_creazione datetime not null,
|
||||||
|
data_modifica datetime not null,
|
||||||
|
utente_modifica varchar(50) not null
|
||||||
|
);
|
||||||
|
|
||||||
|
-- **************************************************** tipo_paragrafo
|
||||||
|
|
||||||
|
create table tipo_paragrafo
|
||||||
|
(
|
||||||
|
id varchar(10) not null primary key,
|
||||||
|
nome varchar(250) not null,
|
||||||
|
data_creazione datetime not null,
|
||||||
|
data_modifica datetime not null,
|
||||||
|
utente_modifica varchar(50) not null
|
||||||
|
);
|
||||||
|
|
||||||
|
-- **************************************************** attivita
|
||||||
|
|
||||||
|
create table attivita
|
||||||
|
(
|
||||||
|
id int not null primary key auto_increment,
|
||||||
|
nome varchar(250) not null,
|
||||||
|
autore varchar(250) not null,
|
||||||
|
padre int null,
|
||||||
|
stato varchar(2) not null,
|
||||||
|
data_creazione datetime not null,
|
||||||
|
data_modifica datetime not null,
|
||||||
|
utente_modifica varchar(50) not null,
|
||||||
|
|
||||||
|
constraint fk_attivita_attivita foreign key (padre) references attivita (id),
|
||||||
|
constraint fk_attivita_stato_attivita foreign key (stato) references stato_attivita (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create index fk_attivita_attivita_idx on attivita (padre);
|
||||||
|
|
||||||
|
create index fk_attivita_stato_attivita_idx on attivita (stato);
|
||||||
|
|
||||||
|
-- **************************************************** branca
|
||||||
|
|
||||||
|
create table branca
|
||||||
|
(
|
||||||
|
id int not null primary key auto_increment,
|
||||||
|
nome varchar(250) not null,
|
||||||
|
inizio_eta int null,
|
||||||
|
fine_eta int null,
|
||||||
|
colore varchar(10) null,
|
||||||
|
data_creazione datetime not null,
|
||||||
|
data_modifica datetime not null,
|
||||||
|
utente_modifica varchar(50) not null
|
||||||
|
);
|
||||||
|
|
||||||
|
-- **************************************************** branca_attivita
|
||||||
|
|
||||||
|
create table branca_attivita
|
||||||
|
(
|
||||||
|
attivita_id int not null,
|
||||||
|
branca_id int not null,
|
||||||
|
cancellato tinyint not null,
|
||||||
|
data_creazione datetime not null,
|
||||||
|
data_modifica datetime not null,
|
||||||
|
utente_modifica varchar(50) not null,
|
||||||
|
|
||||||
|
primary key (attivita_id, branca_id),
|
||||||
|
|
||||||
|
constraint fk_branca_attivita_attivita foreign key (attivita_id) references attivita (id),
|
||||||
|
constraint fk_branca_attivita_branca foreign key (branca_id) references branca (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create index fk_branca_attivita_attivita_idx on branca_attivita (attivita_id);
|
||||||
|
|
||||||
|
create index fk_branca_attivita_branca_idx on branca_attivita (branca_id);
|
||||||
|
|
||||||
|
-- **************************************************** periodo_anno
|
||||||
|
|
||||||
|
create table periodo_anno
|
||||||
|
(
|
||||||
|
id int not null primary key auto_increment,
|
||||||
|
nome varchar(250) not null,
|
||||||
|
inizio_mese int null,
|
||||||
|
fine_mese int null,
|
||||||
|
data_creazione datetime not null,
|
||||||
|
data_modifica datetime not null,
|
||||||
|
utente_modifica varchar(50) not null
|
||||||
|
);
|
||||||
|
|
||||||
|
-- **************************************************** periodo_anno_attivita
|
||||||
|
|
||||||
|
create table periodo_anno_attivita
|
||||||
|
(
|
||||||
|
attivita_id int not null,
|
||||||
|
periodo_anno_id int not null,
|
||||||
|
cancellato tinyint not null,
|
||||||
|
data_creazione datetime not null,
|
||||||
|
data_modifica datetime not null,
|
||||||
|
utente_modifica varchar(50) not null,
|
||||||
|
|
||||||
|
primary key (attivita_id, periodo_anno_id),
|
||||||
|
|
||||||
|
constraint fk_periodo_anno_attivita_attivita foreign key (attivita_id) references attivita (id),
|
||||||
|
constraint fk_periodo_anno_attivita_periodo_anno foreign key (periodo_anno_id) references periodo_anno (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create index fk_periodo_anno_attivita_attivita_idx on periodo_anno_attivita (attivita_id);
|
||||||
|
|
||||||
|
create index fkperiodo_anno_attivita_periodo_anno_idx on periodo_anno_attivita (periodo_anno_id);
|
||||||
|
|
||||||
|
-- **************************************************** paragrafo
|
||||||
|
|
||||||
|
create table paragrafo
|
||||||
|
(
|
||||||
|
id int not null primary key auto_increment,
|
||||||
|
attivita_id int not null,
|
||||||
|
corpo varchar(500) not null,
|
||||||
|
autore varchar(250) not null,
|
||||||
|
tipo varchar(10) not null,
|
||||||
|
ordine int not null,
|
||||||
|
data_creazione datetime not null,
|
||||||
|
data_modifica datetime not null,
|
||||||
|
utente_modifica varchar(50) not null,
|
||||||
|
|
||||||
|
constraint fk_paragrafo_tipo_paragrafo foreign key (tipo) references tipo_paragrafo (id),
|
||||||
|
constraint fk_paragrafo_attivita foreign key (attivita_id) references attivita (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create index fk_paragrafo_tipo_paragrafo_idx on paragrafo (tipo);
|
||||||
|
|
||||||
|
-- **************************************************** categoria
|
||||||
|
|
||||||
|
create table categoria
|
||||||
|
(
|
||||||
|
id int not null primary key auto_increment,
|
||||||
|
nome varchar(250) not null,
|
||||||
|
padre int null,
|
||||||
|
tipo varchar(2) not null,
|
||||||
|
data_creazione datetime not null,
|
||||||
|
data_modifica datetime not null,
|
||||||
|
utente_modifica varchar(50) not null,
|
||||||
|
|
||||||
|
constraint fk_categoria_categoria foreign key (padre) references categoria (id),
|
||||||
|
constraint fk_categoria_tipo_categoria foreign key (tipo) references tipo_categoria (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create index fk_categoria_categoria_idx on categoria (padre);
|
||||||
|
|
||||||
|
create index fk_categoria_tipo_categoria_idx on categoria (tipo);
|
||||||
|
|
||||||
|
-- **************************************************** categoria_attivita
|
||||||
|
|
||||||
|
create table categoria_attivita
|
||||||
|
(
|
||||||
|
attivita_id int not null,
|
||||||
|
categoria_id int not null,
|
||||||
|
cancellato tinyint not null,
|
||||||
|
data_creazione datetime not null,
|
||||||
|
data_modifica datetime not null,
|
||||||
|
utente_modifica varchar(50) not null,
|
||||||
|
|
||||||
|
primary key (attivita_id, categoria_id),
|
||||||
|
|
||||||
|
constraint fk_categoria_attivita_attivita foreign key (attivita_id) references attivita (id),
|
||||||
|
constraint fk_categoria_attivita_categoria foreign key (categoria_id) references categoria (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create index fk_categoria_attivita_attivita_idx on categoria_attivita (attivita_id);
|
||||||
|
|
||||||
|
create index fk_categoria_attivita_categoria_idx on categoria_attivita (categoria_id);
|
||||||
|
|
||||||
|
-- **************************************************** materiale
|
||||||
|
|
||||||
|
create table materiale
|
||||||
|
(
|
||||||
|
id int not null primary key auto_increment,
|
||||||
|
nome varchar(250) not null,
|
||||||
|
proprieta json,
|
||||||
|
data_creazione datetime not null,
|
||||||
|
data_modifica datetime not null,
|
||||||
|
utente_modifica varchar(50) not null
|
||||||
|
);
|
||||||
|
|
||||||
|
-- **************************************************** materiale_attivita
|
||||||
|
|
||||||
|
create table materiale_attivita
|
||||||
|
(
|
||||||
|
attivita_id int not null,
|
||||||
|
materiale_id int not null,
|
||||||
|
proprieta json null,
|
||||||
|
cancellato tinyint not null,
|
||||||
|
data_creazione datetime not null,
|
||||||
|
data_modifica datetime not null,
|
||||||
|
utente_modifica varchar(50) not null,
|
||||||
|
|
||||||
|
primary key (attivita_id, materiale_id),
|
||||||
|
|
||||||
|
constraint fk_materiale_attivita_attivita foreign key (attivita_id) references attivita (id),
|
||||||
|
constraint fk_materiale_attivita_materiale foreign key (materiale_id) references materiale (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create index fk_materiale_attivita_attivita_idx on materiale_attivita (attivita_id);
|
||||||
|
|
||||||
|
create index fk_materiale_attivita_materiale_idx on materiale_attivita (materiale_id);
|
||||||
|
|
||||||
|
-- **************************************************** categoria_materiale
|
||||||
|
|
||||||
|
create table categoria_materiale
|
||||||
|
(
|
||||||
|
materiale_id int not null,
|
||||||
|
categoria_id int not null,
|
||||||
|
cancellato tinyint not null,
|
||||||
|
data_creazione datetime not null,
|
||||||
|
data_modifica datetime not null,
|
||||||
|
utente_modifica varchar(50) not null,
|
||||||
|
|
||||||
|
primary key (materiale_id, categoria_id),
|
||||||
|
|
||||||
|
constraint fk_categoria_materiale_categoria foreign key (categoria_id) references categoria (id),
|
||||||
|
constraint fk_categoria_materiale_materiale foreign key (materiale_id) references materiale (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create index fk_categoria_materiale_categoria_idx on categoria_materiale (categoria_id);
|
||||||
|
|
||||||
|
create index fk_categoria_materiale_materiale_idx on categoria_materiale (materiale_id);
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
INSERT INTO scouthub.stato_attivita (id, nome, data_creazione, data_modifica, utente_modifica) VALUES ('PU', 'Pubblicato', sysdate(), sysdate(), 'MANUALE');
|
||||||
|
INSERT INTO scouthub.stato_attivita (id, nome, data_creazione, data_modifica, utente_modifica) VALUES ('BO', 'Bozza', sysdate(), sysdate(), 'MANUALE');
|
||||||
|
INSERT INTO scouthub.stato_attivita (id, nome, data_creazione, data_modifica, utente_modifica) VALUES ('PR', 'Privato', sysdate(), sysdate(), 'MANUALE');
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
INSERT INTO scouthub.tipo_categoria (id, nome, data_creazione, data_modifica, utente_modifica) VALUES ('A', 'Attività', sysdate(), sysdate(), 'MANUALE');
|
||||||
|
INSERT INTO scouthub.tipo_categoria (id, nome, data_creazione, data_modifica, utente_modifica) VALUES ('M', 'Materiale', sysdate(), sysdate(), 'MANUALE');
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
INSERT INTO scouthub.tipo_paragrafo (id, nome, data_creazione, data_modifica, utente_modifica) VALUES ('TITOLO', 'Titolo attività', sysdate(), sysdate(), 'MANUALE');
|
||||||
|
INSERT INTO scouthub.tipo_paragrafo (id, nome, data_creazione, data_modifica, utente_modifica) VALUES ('PARAGRAFO', 'Paragrafo attività', sysdate(), sysdate(), 'MANUALE');
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
INSERT INTO scouthub.branca (id, nome, inizio_eta, fine_eta, colore, data_creazione, data_modifica, utente_modifica) VALUES (1, 'L/C', 8, 10, '#FDD835', sysdate(), sysdate(), 'MANUALE');
|
||||||
|
INSERT INTO scouthub.branca (id, nome, inizio_eta, fine_eta, colore, data_creazione, data_modifica, utente_modifica) VALUES (2, 'E/G', 11, 16, '#43A047', sysdate(), sysdate(), 'MANUALE');
|
||||||
|
INSERT INTO scouthub.branca (id, nome, inizio_eta, fine_eta, colore, data_creazione, data_modifica, utente_modifica) VALUES (3, 'R/S', 17, 21, '#E53935', sysdate(), sysdate(), 'MANUALE');
|
||||||
|
INSERT INTO scouthub.branca (id, nome, inizio_eta, fine_eta, colore, data_creazione, data_modifica, utente_modifica) VALUES (4, 'Co.Ca.', 22, 99, '#8E24AA', sysdate(), sysdate(), 'MANUALE');
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
INSERT INTO scouthub.periodo_anno (id, nome, inizio_mese, fine_mese, data_creazione, data_modifica, utente_modifica) VALUES (1, 'promessa', 1, 3, sysdate(), sysdate(), 'MANUALE');
|
||||||
|
INSERT INTO scouthub.periodo_anno (id, nome, inizio_mese, fine_mese, data_creazione, data_modifica, utente_modifica) VALUES (2, 'campo estivo', 6, 9, sysdate(), sysdate(), 'MANUALE');
|
||||||
|
INSERT INTO scouthub.periodo_anno (id, nome, inizio_mese, fine_mese, data_creazione, data_modifica, utente_modifica) VALUES (3, 'campo invernale', 12, 1, sysdate(), sysdate(), 'MANUALE');
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
INSERT INTO scouthub.categoria (id, nome, padre, tipo, data_creazione, data_modifica, utente_modifica) VALUES (1, 'attività', null, 'A', sysdate(), sysdate(), 'MANUALE');
|
||||||
|
INSERT INTO scouthub.categoria (id, nome, padre, tipo, data_creazione, data_modifica, utente_modifica) VALUES (2, 'gioco', null, 'A', sysdate(), sysdate(), 'MANUALE');
|
||||||
|
INSERT INTO scouthub.categoria (id, nome, padre, tipo, data_creazione, data_modifica, utente_modifica) VALUES (3, 'danza', null, 'A', sysdate(), sysdate(), 'MANUALE');
|
||||||
|
INSERT INTO scouthub.categoria (id, nome, padre, tipo, data_creazione, data_modifica, utente_modifica) VALUES (4, 'gioco d''acqua', 2, 'A', sysdate(), sysdate(), 'MANUALE');
|
||||||
|
INSERT INTO scouthub.categoria (id, nome, padre, tipo, data_creazione, data_modifica, utente_modifica) VALUES (5, 'gioco notturno', 2, 'A', sysdate(), sysdate(), 'MANUALE');
|
||||||
|
INSERT INTO scouthub.categoria (id, nome, padre, tipo, data_creazione, data_modifica, utente_modifica) VALUES (6, 'grande gioco', 2, 'A', sysdate(), sysdate(), 'MANUALE');
|
||||||
|
INSERT INTO scouthub.categoria (id, nome, padre, tipo, data_creazione, data_modifica, utente_modifica) VALUES (7, 'torneo', 2, 'A', sysdate(), sysdate(), 'MANUALE');
|
||||||
|
INSERT INTO scouthub.categoria (id, nome, padre, tipo, data_creazione, data_modifica, utente_modifica) VALUES (8, 'olimpiadi', 2, 'A', sysdate(), sysdate(), 'MANUALE');
|
||||||
|
INSERT INTO scouthub.categoria (id, nome, padre, tipo, data_creazione, data_modifica, utente_modifica) VALUES (9, 'gioco giungla', 2, 'A', sysdate(), sysdate(), 'MANUALE');
|
||||||
Binary file not shown.
@@ -0,0 +1,16 @@
|
|||||||
|
FROM node:18-bookworm-slim
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y openssl && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npx prisma generate
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
CMD ["sh", "-c", "npx prisma migrate deploy && npx prisma db seed && node dist/server.js"]
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# scouthub-attivita-be
|
||||||
|
|
||||||
|
Backend Node.js/TypeScript/Express/Prisma per la gestione di attivita scout —
|
||||||
|
sostituisce il precedente backend Java/Spring Boot.
|
||||||
|
|
||||||
|
## Prerequisiti
|
||||||
|
|
||||||
|
- Node.js 18+
|
||||||
|
- Un'istanza PostgreSQL raggiungibile (locale o remota)
|
||||||
|
|
||||||
|
## Variabili d'ambiente
|
||||||
|
|
||||||
|
Copiare `.env.example` in `.env` e valorizzare le variabili richieste
|
||||||
|
(`PORT`, `CORS_ORIGIN`, `DATABASE_URL`, `DATABASE_URL_TEST`). Vedere
|
||||||
|
`.env.example` per il formato atteso e i valori di default usati in sviluppo.
|
||||||
|
|
||||||
|
## Sviluppo locale
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npx prisma migrate dev
|
||||||
|
npm run db:seed
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Il server si avvia sulla porta definita da `PORT` (default 8080) e risponde
|
||||||
|
su `GET /health`.
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
Richiede `DATABASE_URL_TEST` configurato in `.env` e puntato a un database
|
||||||
|
PostgreSQL dedicato ai test, con le migration gia' applicate: la suite
|
||||||
|
esegue un `TRUNCATE` delle tabelle tra un test e l'altro, quindi va tenuto
|
||||||
|
separato dal database di sviluppo.
|
||||||
|
|
||||||
|
## Note
|
||||||
|
|
||||||
|
- L'autenticazione reale non e' ancora implementata: l'autore delle attivita'
|
||||||
|
e' ancora hardcoded a `"Movio"` nel router privato, e il middleware
|
||||||
|
`authPlaceholder` e' un pass-through.
|
||||||
|
- Porta e CORS sono gia' letti da variabili d'ambiente in previsione di un
|
||||||
|
futuro `docker-compose.yml` (non incluso in questa feature).
|
||||||
|
|
||||||
|
## Riferimento storico
|
||||||
|
|
||||||
|
La cartella `DDL/` contiene gli script SQL dello schema originale del
|
||||||
|
backend Java (MySQL), mantenuti come riferimento storico. Lo schema attuale
|
||||||
|
(PostgreSQL) e' definito in `prisma/schema.prisma` e gestito tramite Prisma
|
||||||
|
Migrate.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import type { Config } from 'jest';
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
rootDir: '.',
|
||||||
|
testMatch: ['<rootDir>/tests/unit/**/*.test.ts', '<rootDir>/tests/integration/**/*.test.ts'],
|
||||||
|
globalSetup: '<rootDir>/tests/setup/globalSetup.ts',
|
||||||
|
globalTeardown: '<rootDir>/tests/setup/globalTeardown.ts',
|
||||||
|
testTimeout: 15000,
|
||||||
|
transform: {
|
||||||
|
'^.+\\.ts$': ['ts-jest', { tsconfig: 'tsconfig.jest.json' }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
Generated
+6908
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "scouthub-attivita-be",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Backend Node.js/TypeScript per la gestione di attivita scout",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "ts-node-dev --respawn --transpile-only src/server.ts",
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"start": "node dist/server.js",
|
||||||
|
"prisma:generate": "prisma generate",
|
||||||
|
"prisma:migrate": "prisma migrate dev",
|
||||||
|
"db:seed": "prisma db seed",
|
||||||
|
"test": "jest --runInBand --passWithNoTests",
|
||||||
|
"test:watch": "jest --watch --runInBand"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/client": "^5.16.1",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"express": "^4.19.2",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
|
"jwks-rsa": "^3.2.2",
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
|
"@types/node": "^20.14.12",
|
||||||
|
"@types/supertest": "^7.2.1",
|
||||||
|
"jest": "^30.4.2",
|
||||||
|
"nock": "^13.5.4",
|
||||||
|
"prisma": "^5.16.1",
|
||||||
|
"supertest": "^7.2.2",
|
||||||
|
"ts-jest": "^29.4.12",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"ts-node-dev": "^2.0.0",
|
||||||
|
"typescript": "^5.5.4"
|
||||||
|
},
|
||||||
|
"prisma": {
|
||||||
|
"seed": "ts-node prisma/seed.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "stato_attivita" (
|
||||||
|
"id" VARCHAR(2) NOT NULL,
|
||||||
|
"nome" TEXT NOT NULL,
|
||||||
|
"data_creazione" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"data_modifica" TIMESTAMP(3) NOT NULL,
|
||||||
|
"utente_modifica" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "stato_attivita_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "tipo_categoria" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"nome" TEXT NOT NULL,
|
||||||
|
"data_creazione" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"data_modifica" TIMESTAMP(3) NOT NULL,
|
||||||
|
"utente_modifica" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "tipo_categoria_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "tipo_paragrafo" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"nome" TEXT NOT NULL,
|
||||||
|
"data_creazione" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"data_modifica" TIMESTAMP(3) NOT NULL,
|
||||||
|
"utente_modifica" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "tipo_paragrafo_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "attivita" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"nome" TEXT NOT NULL,
|
||||||
|
"autore" TEXT NOT NULL,
|
||||||
|
"padre" INTEGER,
|
||||||
|
"stato" TEXT NOT NULL,
|
||||||
|
"data_creazione" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"data_modifica" TIMESTAMP(3) NOT NULL,
|
||||||
|
"utente_modifica" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "attivita_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "branca" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"nome" TEXT NOT NULL,
|
||||||
|
"inizio_eta" INTEGER,
|
||||||
|
"fine_eta" INTEGER,
|
||||||
|
"colore" VARCHAR(10),
|
||||||
|
"data_creazione" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"data_modifica" TIMESTAMP(3) NOT NULL,
|
||||||
|
"utente_modifica" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "branca_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "branca_attivita" (
|
||||||
|
"attivita_id" INTEGER NOT NULL,
|
||||||
|
"branca_id" INTEGER NOT NULL,
|
||||||
|
"cancellato" BOOLEAN NOT NULL,
|
||||||
|
"data_creazione" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"data_modifica" TIMESTAMP(3) NOT NULL,
|
||||||
|
"utente_modifica" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "branca_attivita_pkey" PRIMARY KEY ("attivita_id","branca_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "periodo_anno" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"nome" TEXT NOT NULL,
|
||||||
|
"inizio_mese" INTEGER,
|
||||||
|
"fine_mese" INTEGER,
|
||||||
|
"data_creazione" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"data_modifica" TIMESTAMP(3) NOT NULL,
|
||||||
|
"utente_modifica" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "periodo_anno_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "periodo_anno_attivita" (
|
||||||
|
"attivita_id" INTEGER NOT NULL,
|
||||||
|
"periodo_anno_id" INTEGER NOT NULL,
|
||||||
|
"cancellato" BOOLEAN NOT NULL,
|
||||||
|
"data_creazione" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"data_modifica" TIMESTAMP(3) NOT NULL,
|
||||||
|
"utente_modifica" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "periodo_anno_attivita_pkey" PRIMARY KEY ("attivita_id","periodo_anno_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "paragrafo" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"attivita_id" INTEGER NOT NULL,
|
||||||
|
"corpo" VARCHAR(500) NOT NULL,
|
||||||
|
"autore" TEXT NOT NULL,
|
||||||
|
"tipo" TEXT NOT NULL,
|
||||||
|
"ordine" INTEGER NOT NULL,
|
||||||
|
"data_creazione" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"data_modifica" TIMESTAMP(3) NOT NULL,
|
||||||
|
"utente_modifica" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "paragrafo_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "categoria" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"nome" TEXT NOT NULL,
|
||||||
|
"padre" INTEGER,
|
||||||
|
"tipo" TEXT NOT NULL,
|
||||||
|
"data_creazione" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"data_modifica" TIMESTAMP(3) NOT NULL,
|
||||||
|
"utente_modifica" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "categoria_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "categoria_attivita" (
|
||||||
|
"attivita_id" INTEGER NOT NULL,
|
||||||
|
"categoria_id" INTEGER NOT NULL,
|
||||||
|
"cancellato" BOOLEAN NOT NULL,
|
||||||
|
"data_creazione" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"data_modifica" TIMESTAMP(3) NOT NULL,
|
||||||
|
"utente_modifica" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "categoria_attivita_pkey" PRIMARY KEY ("attivita_id","categoria_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "materiale" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"nome" TEXT NOT NULL,
|
||||||
|
"proprieta" JSONB,
|
||||||
|
"data_creazione" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"data_modifica" TIMESTAMP(3) NOT NULL,
|
||||||
|
"utente_modifica" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "materiale_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "materiale_attivita" (
|
||||||
|
"attivita_id" INTEGER NOT NULL,
|
||||||
|
"materiale_id" INTEGER NOT NULL,
|
||||||
|
"proprieta" JSONB,
|
||||||
|
"cancellato" BOOLEAN NOT NULL,
|
||||||
|
"data_creazione" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"data_modifica" TIMESTAMP(3) NOT NULL,
|
||||||
|
"utente_modifica" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "materiale_attivita_pkey" PRIMARY KEY ("attivita_id","materiale_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "categoria_materiale" (
|
||||||
|
"materiale_id" INTEGER NOT NULL,
|
||||||
|
"categoria_id" INTEGER NOT NULL,
|
||||||
|
"cancellato" BOOLEAN NOT NULL,
|
||||||
|
"data_creazione" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"data_modifica" TIMESTAMP(3) NOT NULL,
|
||||||
|
"utente_modifica" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "categoria_materiale_pkey" PRIMARY KEY ("materiale_id","categoria_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "attivita" ADD CONSTRAINT "attivita_padre_fkey" FOREIGN KEY ("padre") REFERENCES "attivita"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "attivita" ADD CONSTRAINT "attivita_stato_fkey" FOREIGN KEY ("stato") REFERENCES "stato_attivita"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "branca_attivita" ADD CONSTRAINT "branca_attivita_attivita_id_fkey" FOREIGN KEY ("attivita_id") REFERENCES "attivita"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "branca_attivita" ADD CONSTRAINT "branca_attivita_branca_id_fkey" FOREIGN KEY ("branca_id") REFERENCES "branca"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "periodo_anno_attivita" ADD CONSTRAINT "periodo_anno_attivita_attivita_id_fkey" FOREIGN KEY ("attivita_id") REFERENCES "attivita"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "periodo_anno_attivita" ADD CONSTRAINT "periodo_anno_attivita_periodo_anno_id_fkey" FOREIGN KEY ("periodo_anno_id") REFERENCES "periodo_anno"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "paragrafo" ADD CONSTRAINT "paragrafo_attivita_id_fkey" FOREIGN KEY ("attivita_id") REFERENCES "attivita"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "paragrafo" ADD CONSTRAINT "paragrafo_tipo_fkey" FOREIGN KEY ("tipo") REFERENCES "tipo_paragrafo"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "categoria" ADD CONSTRAINT "categoria_padre_fkey" FOREIGN KEY ("padre") REFERENCES "categoria"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "categoria" ADD CONSTRAINT "categoria_tipo_fkey" FOREIGN KEY ("tipo") REFERENCES "tipo_categoria"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "categoria_attivita" ADD CONSTRAINT "categoria_attivita_attivita_id_fkey" FOREIGN KEY ("attivita_id") REFERENCES "attivita"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "categoria_attivita" ADD CONSTRAINT "categoria_attivita_categoria_id_fkey" FOREIGN KEY ("categoria_id") REFERENCES "categoria"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "materiale_attivita" ADD CONSTRAINT "materiale_attivita_attivita_id_fkey" FOREIGN KEY ("attivita_id") REFERENCES "attivita"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "materiale_attivita" ADD CONSTRAINT "materiale_attivita_materiale_id_fkey" FOREIGN KEY ("materiale_id") REFERENCES "materiale"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "categoria_materiale" ADD CONSTRAINT "categoria_materiale_materiale_id_fkey" FOREIGN KEY ("materiale_id") REFERENCES "materiale"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "categoria_materiale" ADD CONSTRAINT "categoria_materiale_categoria_id_fkey" FOREIGN KEY ("categoria_id") REFERENCES "categoria"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- AlterTable
|
||||||
|
-- Le righe esistenti (dati di sviluppo/seed, precedenti all'introduzione
|
||||||
|
-- dell'autenticazione Keycloak) non hanno un autore reale: valorizzate con
|
||||||
|
-- stringa vuota, poi il default viene rimosso perche' da qui in avanti
|
||||||
|
-- autoreId e' sempre valorizzato dal token (sub) al momento della creazione.
|
||||||
|
ALTER TABLE "attivita" ADD COLUMN "autore_id" TEXT NOT NULL DEFAULT '';
|
||||||
|
ALTER TABLE "attivita" ALTER COLUMN "autore_id" DROP DEFAULT;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "postgresql"
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model StatoAttivita {
|
||||||
|
id String @id @db.VarChar(2)
|
||||||
|
nome String
|
||||||
|
dataCreazione DateTime @default(now()) @map("data_creazione")
|
||||||
|
dataModifica DateTime @updatedAt @map("data_modifica")
|
||||||
|
utenteModifica String @map("utente_modifica")
|
||||||
|
|
||||||
|
attivita Attivita[]
|
||||||
|
|
||||||
|
@@map("stato_attivita")
|
||||||
|
}
|
||||||
|
|
||||||
|
model TipoCategoria {
|
||||||
|
id String @id
|
||||||
|
nome String
|
||||||
|
dataCreazione DateTime @default(now()) @map("data_creazione")
|
||||||
|
dataModifica DateTime @updatedAt @map("data_modifica")
|
||||||
|
utenteModifica String @map("utente_modifica")
|
||||||
|
|
||||||
|
categorie Categoria[]
|
||||||
|
|
||||||
|
@@map("tipo_categoria")
|
||||||
|
}
|
||||||
|
|
||||||
|
model TipoParagrafo {
|
||||||
|
id String @id
|
||||||
|
nome String
|
||||||
|
dataCreazione DateTime @default(now()) @map("data_creazione")
|
||||||
|
dataModifica DateTime @updatedAt @map("data_modifica")
|
||||||
|
utenteModifica String @map("utente_modifica")
|
||||||
|
|
||||||
|
paragrafi Paragrafo[]
|
||||||
|
|
||||||
|
@@map("tipo_paragrafo")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Attivita {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
nome String
|
||||||
|
autore String
|
||||||
|
autoreId String @map("autore_id")
|
||||||
|
padreId Int? @map("padre")
|
||||||
|
statoId String @map("stato")
|
||||||
|
dataCreazione DateTime @default(now()) @map("data_creazione")
|
||||||
|
dataModifica DateTime @updatedAt @map("data_modifica")
|
||||||
|
utenteModifica String @map("utente_modifica")
|
||||||
|
|
||||||
|
padre Attivita? @relation("AttivitaPadre", fields: [padreId], references: [id])
|
||||||
|
figli Attivita[] @relation("AttivitaPadre")
|
||||||
|
stato StatoAttivita @relation(fields: [statoId], references: [id])
|
||||||
|
|
||||||
|
paragrafi Paragrafo[]
|
||||||
|
brancaLinks BrancaAttivita[]
|
||||||
|
categoriaLinks CategoriaAttivita[]
|
||||||
|
materialeLinks MaterialeAttivita[]
|
||||||
|
periodoAnnoLinks PeriodoAnnoAttivita[]
|
||||||
|
|
||||||
|
@@map("attivita")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Branca {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
nome String
|
||||||
|
inizioEta Int? @map("inizio_eta")
|
||||||
|
fineEta Int? @map("fine_eta")
|
||||||
|
colore String? @db.VarChar(10)
|
||||||
|
dataCreazione DateTime @default(now()) @map("data_creazione")
|
||||||
|
dataModifica DateTime @updatedAt @map("data_modifica")
|
||||||
|
utenteModifica String @map("utente_modifica")
|
||||||
|
|
||||||
|
attivitaLinks BrancaAttivita[]
|
||||||
|
|
||||||
|
@@map("branca")
|
||||||
|
}
|
||||||
|
|
||||||
|
model BrancaAttivita {
|
||||||
|
attivitaId Int @map("attivita_id")
|
||||||
|
brancaId Int @map("branca_id")
|
||||||
|
cancellato Boolean
|
||||||
|
dataCreazione DateTime @default(now()) @map("data_creazione")
|
||||||
|
dataModifica DateTime @updatedAt @map("data_modifica")
|
||||||
|
utenteModifica String @map("utente_modifica")
|
||||||
|
|
||||||
|
attivita Attivita @relation(fields: [attivitaId], references: [id])
|
||||||
|
branca Branca @relation(fields: [brancaId], references: [id])
|
||||||
|
|
||||||
|
@@id([attivitaId, brancaId])
|
||||||
|
@@map("branca_attivita")
|
||||||
|
}
|
||||||
|
|
||||||
|
model PeriodoAnno {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
nome String
|
||||||
|
inizioMese Int? @map("inizio_mese")
|
||||||
|
fineMese Int? @map("fine_mese")
|
||||||
|
dataCreazione DateTime @default(now()) @map("data_creazione")
|
||||||
|
dataModifica DateTime @updatedAt @map("data_modifica")
|
||||||
|
utenteModifica String @map("utente_modifica")
|
||||||
|
|
||||||
|
attivitaLinks PeriodoAnnoAttivita[]
|
||||||
|
|
||||||
|
@@map("periodo_anno")
|
||||||
|
}
|
||||||
|
|
||||||
|
model PeriodoAnnoAttivita {
|
||||||
|
attivitaId Int @map("attivita_id")
|
||||||
|
periodoAnnoId Int @map("periodo_anno_id")
|
||||||
|
cancellato Boolean
|
||||||
|
dataCreazione DateTime @default(now()) @map("data_creazione")
|
||||||
|
dataModifica DateTime @updatedAt @map("data_modifica")
|
||||||
|
utenteModifica String @map("utente_modifica")
|
||||||
|
|
||||||
|
attivita Attivita @relation(fields: [attivitaId], references: [id])
|
||||||
|
periodoAnno PeriodoAnno @relation(fields: [periodoAnnoId], references: [id])
|
||||||
|
|
||||||
|
@@id([attivitaId, periodoAnnoId])
|
||||||
|
@@map("periodo_anno_attivita")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Paragrafo {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
attivitaId Int @map("attivita_id")
|
||||||
|
corpo String @db.VarChar(500)
|
||||||
|
autore String
|
||||||
|
tipoId String @map("tipo")
|
||||||
|
ordine Int
|
||||||
|
dataCreazione DateTime @default(now()) @map("data_creazione")
|
||||||
|
dataModifica DateTime @updatedAt @map("data_modifica")
|
||||||
|
utenteModifica String @map("utente_modifica")
|
||||||
|
|
||||||
|
attivita Attivita @relation(fields: [attivitaId], references: [id])
|
||||||
|
tipo TipoParagrafo @relation(fields: [tipoId], references: [id])
|
||||||
|
|
||||||
|
@@map("paragrafo")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Categoria {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
nome String
|
||||||
|
padreId Int? @map("padre")
|
||||||
|
tipoId String @map("tipo")
|
||||||
|
dataCreazione DateTime @default(now()) @map("data_creazione")
|
||||||
|
dataModifica DateTime @updatedAt @map("data_modifica")
|
||||||
|
utenteModifica String @map("utente_modifica")
|
||||||
|
|
||||||
|
padre Categoria? @relation("CategoriaPadre", fields: [padreId], references: [id])
|
||||||
|
figli Categoria[] @relation("CategoriaPadre")
|
||||||
|
tipo TipoCategoria @relation(fields: [tipoId], references: [id])
|
||||||
|
|
||||||
|
attivitaLinks CategoriaAttivita[]
|
||||||
|
materialeLinks CategoriaMateriale[]
|
||||||
|
|
||||||
|
@@map("categoria")
|
||||||
|
}
|
||||||
|
|
||||||
|
model CategoriaAttivita {
|
||||||
|
attivitaId Int @map("attivita_id")
|
||||||
|
categoriaId Int @map("categoria_id")
|
||||||
|
cancellato Boolean
|
||||||
|
dataCreazione DateTime @default(now()) @map("data_creazione")
|
||||||
|
dataModifica DateTime @updatedAt @map("data_modifica")
|
||||||
|
utenteModifica String @map("utente_modifica")
|
||||||
|
|
||||||
|
attivita Attivita @relation(fields: [attivitaId], references: [id])
|
||||||
|
categoria Categoria @relation(fields: [categoriaId], references: [id])
|
||||||
|
|
||||||
|
@@id([attivitaId, categoriaId])
|
||||||
|
@@map("categoria_attivita")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Materiale {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
nome String
|
||||||
|
proprieta Json?
|
||||||
|
dataCreazione DateTime @default(now()) @map("data_creazione")
|
||||||
|
dataModifica DateTime @updatedAt @map("data_modifica")
|
||||||
|
utenteModifica String @map("utente_modifica")
|
||||||
|
|
||||||
|
attivitaLinks MaterialeAttivita[]
|
||||||
|
categoriaLinks CategoriaMateriale[]
|
||||||
|
|
||||||
|
@@map("materiale")
|
||||||
|
}
|
||||||
|
|
||||||
|
model MaterialeAttivita {
|
||||||
|
attivitaId Int @map("attivita_id")
|
||||||
|
materialeId Int @map("materiale_id")
|
||||||
|
proprieta Json?
|
||||||
|
cancellato Boolean
|
||||||
|
dataCreazione DateTime @default(now()) @map("data_creazione")
|
||||||
|
dataModifica DateTime @updatedAt @map("data_modifica")
|
||||||
|
utenteModifica String @map("utente_modifica")
|
||||||
|
|
||||||
|
attivita Attivita @relation(fields: [attivitaId], references: [id])
|
||||||
|
materiale Materiale @relation(fields: [materialeId], references: [id])
|
||||||
|
|
||||||
|
@@id([attivitaId, materialeId])
|
||||||
|
@@map("materiale_attivita")
|
||||||
|
}
|
||||||
|
|
||||||
|
model CategoriaMateriale {
|
||||||
|
materialeId Int @map("materiale_id")
|
||||||
|
categoriaId Int @map("categoria_id")
|
||||||
|
cancellato Boolean
|
||||||
|
dataCreazione DateTime @default(now()) @map("data_creazione")
|
||||||
|
dataModifica DateTime @updatedAt @map("data_modifica")
|
||||||
|
utenteModifica String @map("utente_modifica")
|
||||||
|
|
||||||
|
materiale Materiale @relation(fields: [materialeId], references: [id])
|
||||||
|
categoria Categoria @relation(fields: [categoriaId], references: [id])
|
||||||
|
|
||||||
|
@@id([materialeId, categoriaId])
|
||||||
|
@@map("categoria_materiale")
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
const UTENTE_MODIFICA = "MANUALE";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await Promise.all(
|
||||||
|
[
|
||||||
|
{ id: "PU", nome: "Pubblicato" },
|
||||||
|
{ id: "BO", nome: "Bozza" },
|
||||||
|
{ id: "PR", nome: "Privato" },
|
||||||
|
].map((stato) =>
|
||||||
|
prisma.statoAttivita.upsert({
|
||||||
|
where: { id: stato.id },
|
||||||
|
update: { nome: stato.nome, utenteModifica: UTENTE_MODIFICA },
|
||||||
|
create: { ...stato, utenteModifica: UTENTE_MODIFICA },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[
|
||||||
|
{ id: "A", nome: "Attività" },
|
||||||
|
{ id: "M", nome: "Materiale" },
|
||||||
|
].map((tipo) =>
|
||||||
|
prisma.tipoCategoria.upsert({
|
||||||
|
where: { id: tipo.id },
|
||||||
|
update: { nome: tipo.nome, utenteModifica: UTENTE_MODIFICA },
|
||||||
|
create: { ...tipo, utenteModifica: UTENTE_MODIFICA },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[
|
||||||
|
{ id: "TITOLO", nome: "Titolo attività" },
|
||||||
|
{ id: "PARAGRAFO", nome: "Paragrafo attività" },
|
||||||
|
].map((tipo) =>
|
||||||
|
prisma.tipoParagrafo.upsert({
|
||||||
|
where: { id: tipo.id },
|
||||||
|
update: { nome: tipo.nome, utenteModifica: UTENTE_MODIFICA },
|
||||||
|
create: { ...tipo, utenteModifica: UTENTE_MODIFICA },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[
|
||||||
|
{ id: 1, nome: "L/C", inizioEta: 8, fineEta: 10, colore: "#FDD835" },
|
||||||
|
{ id: 2, nome: "E/G", inizioEta: 11, fineEta: 16, colore: "#43A047" },
|
||||||
|
{ id: 3, nome: "R/S", inizioEta: 17, fineEta: 21, colore: "#E53935" },
|
||||||
|
{ id: 4, nome: "Co.Ca.", inizioEta: 22, fineEta: 99, colore: "#8E24AA" },
|
||||||
|
].map((branca) =>
|
||||||
|
prisma.branca.upsert({
|
||||||
|
where: { id: branca.id },
|
||||||
|
update: { ...branca, utenteModifica: UTENTE_MODIFICA },
|
||||||
|
create: { ...branca, utenteModifica: UTENTE_MODIFICA },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[
|
||||||
|
{ id: 1, nome: "promessa", inizioMese: 1, fineMese: 3 },
|
||||||
|
{ id: 2, nome: "campo estivo", inizioMese: 6, fineMese: 9 },
|
||||||
|
{ id: 3, nome: "campo invernale", inizioMese: 12, fineMese: 1 },
|
||||||
|
].map((periodo) =>
|
||||||
|
prisma.periodoAnno.upsert({
|
||||||
|
where: { id: periodo.id },
|
||||||
|
update: { ...periodo, utenteModifica: UTENTE_MODIFICA },
|
||||||
|
create: { ...periodo, utenteModifica: UTENTE_MODIFICA },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const categorieSenzaPadre = [
|
||||||
|
{ id: 1, nome: "attività", tipoId: "A" },
|
||||||
|
{ id: 2, nome: "gioco", tipoId: "A" },
|
||||||
|
{ id: 3, nome: "danza", tipoId: "A" },
|
||||||
|
];
|
||||||
|
for (const categoria of categorieSenzaPadre) {
|
||||||
|
await prisma.categoria.upsert({
|
||||||
|
where: { id: categoria.id },
|
||||||
|
update: { ...categoria, padreId: null, utenteModifica: UTENTE_MODIFICA },
|
||||||
|
create: { ...categoria, utenteModifica: UTENTE_MODIFICA },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const categorieConPadre = [
|
||||||
|
{ id: 4, nome: "gioco d'acqua", tipoId: "A", padreId: 2 },
|
||||||
|
{ id: 5, nome: "gioco notturno", tipoId: "A", padreId: 2 },
|
||||||
|
{ id: 6, nome: "grande gioco", tipoId: "A", padreId: 2 },
|
||||||
|
{ id: 7, nome: "torneo", tipoId: "A", padreId: 2 },
|
||||||
|
{ id: 8, nome: "olimpiadi", tipoId: "A", padreId: 2 },
|
||||||
|
{ id: 9, nome: "gioco giungla", tipoId: "A", padreId: 2 },
|
||||||
|
];
|
||||||
|
for (const categoria of categorieConPadre) {
|
||||||
|
await prisma.categoria.upsert({
|
||||||
|
where: { id: categoria.id },
|
||||||
|
update: { ...categoria, utenteModifica: UTENTE_MODIFICA },
|
||||||
|
create: { ...categoria, utenteModifica: UTENTE_MODIFICA },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
@@ -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'),
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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})`);
|
||||||
|
});
|
||||||
@@ -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
@@ -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>;
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
import request from 'supertest';
|
||||||
|
import nock from 'nock';
|
||||||
|
import { app } from '../../src/app';
|
||||||
|
import { prisma } from '../../src/db/prisma';
|
||||||
|
import { resetDatabase } from '../setup/resetDatabase';
|
||||||
|
import { bearer, mockKeycloakJwks } from '../setup/authTestHelper';
|
||||||
|
|
||||||
|
const AUTORE = { sub: 'user-movio', email: 'movio@example.com', name: 'Movio' };
|
||||||
|
const ALTRO_AUTORE = { sub: 'user-altro', email: 'altro@example.com', name: 'Altro Utente' };
|
||||||
|
const AUTH_HEADER = bearer(AUTORE);
|
||||||
|
const ALTRO_AUTH_HEADER = bearer(ALTRO_AUTORE);
|
||||||
|
|
||||||
|
function attivitaPayload(overrides: Record<string, unknown> = {}) {
|
||||||
|
return {
|
||||||
|
nome: 'Caccia al tesoro',
|
||||||
|
stato: { id: 'BO', nome: 'Bozza' },
|
||||||
|
brancaList: [{ nome: 'E/G' }],
|
||||||
|
categoriaList: [{ nome: 'grande gioco' }],
|
||||||
|
materialeList: [],
|
||||||
|
periodoAnnoList: [],
|
||||||
|
paragrafoList: [{ corpo: 'Introduzione al gioco', tipo: { id: 'PARAGRAFO' }, ordine: 1 }],
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
mockKeycloakJwks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
nock.cleanAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await resetDatabase();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await resetDatabase();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('attivita endpoints', () => {
|
||||||
|
test('GET /health risponde 200 con { status: "ok" }', async () => {
|
||||||
|
const res = await request(app).get('/health');
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body).toEqual({ status: 'ok' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GET /public/attivita/get/lista/home su DB vuoto restituisce []', async () => {
|
||||||
|
const res = await request(app).get('/public/attivita/get/lista/home');
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GET /public/attivita/get/one/:id con id non numerico restituisce 400', async () => {
|
||||||
|
const res = await request(app).get('/public/attivita/get/one/abc');
|
||||||
|
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GET /public/attivita/get/one/:id con id inesistente restituisce 404', async () => {
|
||||||
|
const res = await request(app).get('/public/attivita/get/one/999999');
|
||||||
|
|
||||||
|
expect(res.status).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('endpoint privati senza token restituiscono 401', async () => {
|
||||||
|
const saveRes = await request(app).post('/private/attivita/save').send(attivitaPayload());
|
||||||
|
expect(saveRes.status).toBe(401);
|
||||||
|
|
||||||
|
const myRes = await request(app).get('/private/attivita/get/lista/my');
|
||||||
|
expect(myRes.status).toBe(401);
|
||||||
|
|
||||||
|
const changeRes = await request(app).get('/private/attivita/change/stato/1/PU');
|
||||||
|
expect(changeRes.status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('POST /private/attivita/save senza nome restituisce 400 con informazioni di validazione', async () => {
|
||||||
|
const { nome, ...payloadSenzaNome } = attivitaPayload();
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/private/attivita/save')
|
||||||
|
.set('Authorization', AUTH_HEADER)
|
||||||
|
.send(payloadSenzaNome);
|
||||||
|
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
expect(typeof res.body.message).toBe('string');
|
||||||
|
expect(res.body.message.length).toBeGreaterThan(0);
|
||||||
|
expect(res.body.message).toContain('nome');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("POST /private/attivita/save con payload valido crea l'attività, visibile poi via get/lista/my", async () => {
|
||||||
|
const saveRes = await request(app)
|
||||||
|
.post('/private/attivita/save')
|
||||||
|
.set('Authorization', AUTH_HEADER)
|
||||||
|
.send(attivitaPayload());
|
||||||
|
expect([200, 204]).toContain(saveRes.status);
|
||||||
|
|
||||||
|
const myRes = await request(app)
|
||||||
|
.get('/private/attivita/get/lista/my')
|
||||||
|
.set('Authorization', AUTH_HEADER);
|
||||||
|
expect(myRes.status).toBe(200);
|
||||||
|
expect(myRes.body).toHaveLength(1);
|
||||||
|
expect(myRes.body[0]).toMatchObject({ nome: 'Caccia al tesoro', autore: 'Movio' });
|
||||||
|
expect(myRes.body[0].brancaList.map((b: { nome: string }) => b.nome)).toEqual(['E/G']);
|
||||||
|
expect(myRes.body[0].categoriaList.map((c: { nome: string }) => c.nome)).toEqual([
|
||||||
|
'grande gioco',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('get/lista/my non restituisce le attività di un altro autore', async () => {
|
||||||
|
await request(app).post('/private/attivita/save').set('Authorization', AUTH_HEADER).send(attivitaPayload());
|
||||||
|
|
||||||
|
const myRes = await request(app)
|
||||||
|
.get('/private/attivita/get/lista/my')
|
||||||
|
.set('Authorization', ALTRO_AUTH_HEADER);
|
||||||
|
|
||||||
|
expect(myRes.status).toBe(200);
|
||||||
|
expect(myRes.body).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("POST /private/attivita/save su un'attività di un altro autore restituisce 403", async () => {
|
||||||
|
await request(app).post('/private/attivita/save').set('Authorization', AUTH_HEADER).send(attivitaPayload());
|
||||||
|
const created = await prisma.attivita.findFirstOrThrow();
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/private/attivita/save')
|
||||||
|
.set('Authorization', ALTRO_AUTH_HEADER)
|
||||||
|
.send(attivitaPayload({ id: created.id, nome: 'Modificata da altri' }));
|
||||||
|
|
||||||
|
expect(res.status).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("change/stato aggiorna lo stato e rende visibile l'attività in home", async () => {
|
||||||
|
await request(app).post('/private/attivita/save').set('Authorization', AUTH_HEADER).send(attivitaPayload());
|
||||||
|
const created = await prisma.attivita.findFirstOrThrow();
|
||||||
|
|
||||||
|
const changeRes = await request(app)
|
||||||
|
.get(`/private/attivita/change/stato/${created.id}/PU`)
|
||||||
|
.set('Authorization', AUTH_HEADER);
|
||||||
|
expect(changeRes.status).toBe(200);
|
||||||
|
expect(changeRes.body).toEqual({ id: 'PU', nome: 'Pubblicato' });
|
||||||
|
|
||||||
|
const homeRes = await request(app).get('/public/attivita/get/lista/home');
|
||||||
|
expect(homeRes.status).toBe(200);
|
||||||
|
expect(homeRes.body.map((a: { id: number }) => a.id)).toContain(created.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('change/stato su id inesistente restituisce 404', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/private/attivita/change/stato/999999/PU')
|
||||||
|
.set('Authorization', AUTH_HEADER);
|
||||||
|
|
||||||
|
expect(res.status).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("change/stato su un'attività di un altro autore restituisce 403", async () => {
|
||||||
|
await request(app).post('/private/attivita/save').set('Authorization', AUTH_HEADER).send(attivitaPayload());
|
||||||
|
const created = await prisma.attivita.findFirstOrThrow();
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.get(`/private/attivita/change/stato/${created.id}/PU`)
|
||||||
|
.set('Authorization', ALTRO_AUTH_HEADER);
|
||||||
|
|
||||||
|
expect(res.status).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CORS: risponde con Access-Control-Allow-Origin per un Origin autorizzato', async () => {
|
||||||
|
const res = await request(app).get('/health').set('Origin', 'http://localhost:4200');
|
||||||
|
|
||||||
|
expect(res.headers['access-control-allow-origin']).toBe('http://localhost:4200');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('POST /public/attivita/get/lista/search con [] restituisce tutte le attività presenti', async () => {
|
||||||
|
await request(app)
|
||||||
|
.post('/private/attivita/save')
|
||||||
|
.set('Authorization', AUTH_HEADER)
|
||||||
|
.send(attivitaPayload({ nome: 'Prima' }));
|
||||||
|
await request(app)
|
||||||
|
.post('/private/attivita/save')
|
||||||
|
.set('Authorization', AUTH_HEADER)
|
||||||
|
.send(attivitaPayload({ nome: 'Seconda', stato: { id: 'PU', nome: 'Pubblicato' } }));
|
||||||
|
|
||||||
|
const res = await request(app).post('/public/attivita/get/lista/search').send([]);
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body).toHaveLength(2);
|
||||||
|
expect(res.body.map((a: { nome: string }) => a.nome).sort()).toEqual(['Prima', 'Seconda']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import request from 'supertest';
|
||||||
|
import { app } from '../../src/app';
|
||||||
|
import { prisma } from '../../src/db/prisma';
|
||||||
|
|
||||||
|
// L'unica tabella che questi endpoint (read-only sulle anagrafiche) potrebbero
|
||||||
|
// trovare "sporca" e' `materiale` (il seed non ne inserisce nessuno): la
|
||||||
|
// puliamo prima di ogni test per non dipendere dall'ordine di esecuzione
|
||||||
|
// rispetto alle altre suite.
|
||||||
|
beforeEach(async () => {
|
||||||
|
await prisma.materiale.deleteMany();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('autocomplete endpoints', () => {
|
||||||
|
test('POST /public/autocomplete/get/search senza body contiene il gruppo Testo', async () => {
|
||||||
|
const res = await request(app).post('/public/autocomplete/get/search');
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.some((g: { label: string }) => g.label === 'Testo')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('POST /public/autocomplete/get/search con "gioco" contiene Categoria con le sotto-categorie', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/public/autocomplete/get/search')
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.send('"gioco"');
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const categoria = res.body.find((g: { label: string }) => g.label === 'Categoria');
|
||||||
|
expect(categoria).toBeDefined();
|
||||||
|
expect(categoria.objectsList.map((o: { nome: string }) => o.nome).sort()).toEqual(
|
||||||
|
["gioco", "gioco d'acqua", 'gioco giungla', 'gioco notturno', 'grande gioco'].sort(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('POST /public/autocomplete/get/branca con "e/g" restituisce un solo elemento E/G', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/public/autocomplete/get/branca')
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.send('"e/g"');
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body).toHaveLength(1);
|
||||||
|
expect(res.body[0]).toMatchObject({ nome: 'E/G', gruppo: 'branca' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('POST /public/autocomplete/get/materiale senza body restituisce array vuoto', async () => {
|
||||||
|
const res = await request(app).post('/public/autocomplete/get/materiale');
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { generateKeyPairSync } from 'crypto';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import nock from 'nock';
|
||||||
|
import { env } from '../../src/config/env';
|
||||||
|
|
||||||
|
const KID = 'test-kid';
|
||||||
|
const CERTS_PATH = `/realms/${env.keycloak.realm}/protocol/openid-connect/certs`;
|
||||||
|
|
||||||
|
const { publicKey, privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
|
||||||
|
const jwk = publicKey.export({ format: 'jwk' }) as Record<string, unknown>;
|
||||||
|
const privateKeyPem = privateKey.export({ type: 'pkcs1', format: 'pem' }) as string;
|
||||||
|
|
||||||
|
const { privateKey: rogueKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
|
||||||
|
export const rogueKeyPem = rogueKey.export({ type: 'pkcs1', format: 'pem' }) as string;
|
||||||
|
|
||||||
|
export function mockKeycloakJwks(): void {
|
||||||
|
nock(env.keycloak.baseUrl)
|
||||||
|
.persist()
|
||||||
|
.get(CERTS_PATH)
|
||||||
|
.reply(200, { keys: [{ ...jwk, kid: KID, alg: 'RS256', use: 'sig' }] });
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TokenPayload {
|
||||||
|
sub: string;
|
||||||
|
email?: string;
|
||||||
|
name?: string;
|
||||||
|
realm_access?: { roles?: string[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function signValidToken(payload: TokenPayload, signOptions: jwt.SignOptions = {}): string {
|
||||||
|
return jwt.sign(payload, privateKeyPem, {
|
||||||
|
algorithm: 'RS256',
|
||||||
|
keyid: KID,
|
||||||
|
expiresIn: '5m',
|
||||||
|
...signOptions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bearer(payload: TokenPayload, signOptions: jwt.SignOptions = {}): string {
|
||||||
|
return `Bearer ${signValidToken(payload, signOptions)}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { execSync } from 'child_process';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
export default async function globalSetup(): Promise<void> {
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const databaseUrlTest = process.env.DATABASE_URL_TEST;
|
||||||
|
if (!databaseUrlTest) {
|
||||||
|
throw new Error("Variabile d'ambiente mancante: DATABASE_URL_TEST");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fa puntare tutto il codice applicativo (incluso src/db/prisma.ts) al DB di test,
|
||||||
|
// senza bisogno di modifiche altrove: dotenv.config() non sovrascrive DATABASE_URL
|
||||||
|
// gia' impostata qui.
|
||||||
|
process.env.DATABASE_URL = databaseUrlTest;
|
||||||
|
|
||||||
|
execSync('npx prisma migrate deploy', {
|
||||||
|
env: process.env,
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
|
||||||
|
execSync('npx prisma db seed', {
|
||||||
|
env: process.env,
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { prisma } from '../../src/db/prisma';
|
||||||
|
|
||||||
|
export default async function globalTeardown(): Promise<void> {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { prisma } from '../../src/db/prisma';
|
||||||
|
|
||||||
|
// Solo le tabelle "transazionali" popolate dai test: le anagrafiche di base
|
||||||
|
// seedate una volta sola (stato_attivita, tipo_categoria, tipo_paragrafo,
|
||||||
|
// branca, periodo_anno, categoria, materiale) restano intatte tra un test e l'altro.
|
||||||
|
const TABLES_TO_RESET = [
|
||||||
|
'paragrafo',
|
||||||
|
'branca_attivita',
|
||||||
|
'categoria_attivita',
|
||||||
|
'materiale_attivita',
|
||||||
|
'periodo_anno_attivita',
|
||||||
|
'attivita',
|
||||||
|
];
|
||||||
|
|
||||||
|
export async function resetDatabase(): Promise<void> {
|
||||||
|
const tables = TABLES_TO_RESET.map((table) => `"${table}"`).join(', ');
|
||||||
|
await prisma.$executeRawUnsafe(`TRUNCATE TABLE ${tables} RESTART IDENTITY CASCADE`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
import { prisma } from '../../src/db/prisma';
|
||||||
|
import { resetDatabase } from '../setup/resetDatabase';
|
||||||
|
import {
|
||||||
|
changeStato,
|
||||||
|
getListaHome,
|
||||||
|
getListaSearch,
|
||||||
|
getListMy,
|
||||||
|
getOne,
|
||||||
|
save,
|
||||||
|
} from '../../src/modules/attivita/attivita.service';
|
||||||
|
import { AuthContext } from '../../src/middlewares/auth.types';
|
||||||
|
import { AttivitaSaveInput } from '../../src/types/validation';
|
||||||
|
|
||||||
|
// Questi test girano contro un DB Postgres di test reale (vedi tests/setup),
|
||||||
|
// non contro dei mock: il dominio ha query relazionali/JSON troppo specifiche
|
||||||
|
// per essere simulate in modo affidabile.
|
||||||
|
|
||||||
|
const AUTH: AuthContext = { userId: 'user-test', email: 'test@example.com', name: 'Autore Test', roles: [] };
|
||||||
|
const MARIO: AuthContext = { userId: 'user-mario', email: 'mario@example.com', name: 'Mario', roles: [] };
|
||||||
|
const LUIGI: AuthContext = { userId: 'user-luigi', email: 'luigi@example.com', name: 'Luigi', roles: [] };
|
||||||
|
|
||||||
|
function baseAttivita(overrides: Partial<AttivitaSaveInput> = {}): AttivitaSaveInput {
|
||||||
|
return {
|
||||||
|
nome: 'Attività di test',
|
||||||
|
stato: { id: 'BO', nome: 'Bozza' },
|
||||||
|
brancaList: [],
|
||||||
|
categoriaList: [],
|
||||||
|
materialeList: [],
|
||||||
|
periodoAnnoList: [],
|
||||||
|
paragrafoList: [],
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await resetDatabase();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await resetDatabase();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('attivita.service', () => {
|
||||||
|
test('save collega una branca esistente per nome senza duplicarla', async () => {
|
||||||
|
await save(baseAttivita({ brancaList: [{ nome: 'E/G' }] }), AUTH);
|
||||||
|
|
||||||
|
const brancheEG = await prisma.branca.findMany({ where: { nome: 'E/G' } });
|
||||||
|
expect(brancheEG).toHaveLength(1);
|
||||||
|
|
||||||
|
const attivita = await prisma.attivita.findFirstOrThrow();
|
||||||
|
const link = await prisma.brancaAttivita.findUnique({
|
||||||
|
where: { attivitaId_brancaId: { attivitaId: attivita.id, brancaId: brancheEG[0].id } },
|
||||||
|
});
|
||||||
|
expect(link).not.toBeNull();
|
||||||
|
expect(link?.cancellato).toBe(false);
|
||||||
|
|
||||||
|
const dto = await getOne(attivita.id);
|
||||||
|
expect(dto?.brancaList.map((b) => b.nome)).toEqual(['E/G']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('save crea branca/categoria/materiale/periodoAnno nuovi quando non esistono per nome', async () => {
|
||||||
|
await save(
|
||||||
|
baseAttivita({
|
||||||
|
materialeList: [{ nome: 'Corda 10m', proprieta: { lunghezza: 10 } }],
|
||||||
|
}),
|
||||||
|
AUTH,
|
||||||
|
);
|
||||||
|
|
||||||
|
const materiale = await prisma.materiale.findFirst({ where: { nome: 'Corda 10m' } });
|
||||||
|
expect(materiale).not.toBeNull();
|
||||||
|
|
||||||
|
const attivita = await prisma.attivita.findFirstOrThrow();
|
||||||
|
const link = await prisma.materialeAttivita.findUnique({
|
||||||
|
where: { attivitaId_materialeId: { attivitaId: attivita.id, materialeId: materiale!.id } },
|
||||||
|
});
|
||||||
|
expect(link).not.toBeNull();
|
||||||
|
expect(link?.cancellato).toBe(false);
|
||||||
|
expect(link?.proprieta).toEqual({ lunghezza: 10 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('save in aggiornamento marca come cancellato (soft-delete) il collegamento non più presente', async () => {
|
||||||
|
const lc = await prisma.branca.findFirstOrThrow({ where: { nome: 'L/C' } });
|
||||||
|
const eg = await prisma.branca.findFirstOrThrow({ where: { nome: 'E/G' } });
|
||||||
|
|
||||||
|
await save(baseAttivita({ brancaList: [{ nome: 'L/C' }, { nome: 'E/G' }] }), AUTH);
|
||||||
|
const created = await prisma.attivita.findFirstOrThrow();
|
||||||
|
|
||||||
|
await save(baseAttivita({ id: created.id, brancaList: [{ nome: 'E/G' }] }), AUTH);
|
||||||
|
|
||||||
|
const linkLc = await prisma.brancaAttivita.findUniqueOrThrow({
|
||||||
|
where: { attivitaId_brancaId: { attivitaId: created.id, brancaId: lc.id } },
|
||||||
|
});
|
||||||
|
const linkEg = await prisma.brancaAttivita.findUniqueOrThrow({
|
||||||
|
where: { attivitaId_brancaId: { attivitaId: created.id, brancaId: eg.id } },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(linkLc.cancellato).toBe(true);
|
||||||
|
expect(linkEg.cancellato).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('save sostituisce correttamente i paragrafi (modifica, rimozione, aggiunta)', async () => {
|
||||||
|
await save(
|
||||||
|
baseAttivita({
|
||||||
|
paragrafoList: [
|
||||||
|
{ corpo: 'Paragrafo uno', tipo: { id: 'PARAGRAFO' }, ordine: 1 },
|
||||||
|
{ corpo: 'Paragrafo due', tipo: { id: 'PARAGRAFO' }, ordine: 2 },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
AUTH,
|
||||||
|
);
|
||||||
|
|
||||||
|
const created = await prisma.attivita.findFirstOrThrow();
|
||||||
|
const paragrafi = await prisma.paragrafo.findMany({
|
||||||
|
where: { attivitaId: created.id },
|
||||||
|
orderBy: { ordine: 'asc' },
|
||||||
|
});
|
||||||
|
expect(paragrafi).toHaveLength(2);
|
||||||
|
const [primo, secondo] = paragrafi;
|
||||||
|
|
||||||
|
await save(
|
||||||
|
baseAttivita({
|
||||||
|
id: created.id,
|
||||||
|
paragrafoList: [
|
||||||
|
{
|
||||||
|
id: primo.id,
|
||||||
|
corpo: 'Paragrafo uno modificato',
|
||||||
|
tipo: { id: 'PARAGRAFO' },
|
||||||
|
ordine: 1,
|
||||||
|
},
|
||||||
|
{ corpo: 'Paragrafo nuovo', tipo: { id: 'PARAGRAFO' }, ordine: 2 },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
AUTH,
|
||||||
|
);
|
||||||
|
|
||||||
|
const paragrafiFinali = await prisma.paragrafo.findMany({
|
||||||
|
where: { attivitaId: created.id },
|
||||||
|
orderBy: { ordine: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(paragrafiFinali).toHaveLength(2);
|
||||||
|
expect(paragrafiFinali.map((p) => p.corpo)).toEqual(['Paragrafo uno modificato', 'Paragrafo nuovo']);
|
||||||
|
expect(paragrafiFinali.find((p) => p.id === secondo.id)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('save su id inesistente lancia HttpError 404', async () => {
|
||||||
|
await expect(save(baseAttivita({ id: 999999 }), AUTH)).rejects.toMatchObject({ statusCode: 404 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("save su un'attività di un altro autore lancia HttpError 403", async () => {
|
||||||
|
await save(baseAttivita(), MARIO);
|
||||||
|
const created = await prisma.attivita.findFirstOrThrow();
|
||||||
|
|
||||||
|
await expect(save(baseAttivita({ id: created.id }), LUIGI)).rejects.toMatchObject({ statusCode: 403 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getListaHome restituisce solo attività pubblicate', async () => {
|
||||||
|
await save(baseAttivita({ nome: 'Bozza', stato: { id: 'BO', nome: 'Bozza' } }), AUTH);
|
||||||
|
await save(baseAttivita({ nome: 'Pubblicata', stato: { id: 'PU', nome: 'Pubblicato' } }), AUTH);
|
||||||
|
await save(baseAttivita({ nome: 'Privata', stato: { id: 'PR', nome: 'Privato' } }), AUTH);
|
||||||
|
|
||||||
|
const home = await getListaHome();
|
||||||
|
expect(home).toHaveLength(1);
|
||||||
|
expect(home[0].nome).toBe('Pubblicata');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getListMy filtra per autore', async () => {
|
||||||
|
await save(baseAttivita({ nome: 'Di Mario' }), MARIO);
|
||||||
|
await save(baseAttivita({ nome: 'Di Luigi' }), LUIGI);
|
||||||
|
|
||||||
|
const mie = await getListMy(MARIO.userId);
|
||||||
|
expect(mie).toHaveLength(1);
|
||||||
|
expect(mie[0].nome).toBe('Di Mario');
|
||||||
|
expect(mie[0].autore).toBe('Mario');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('changeStato su id inesistente lancia HttpError 404', async () => {
|
||||||
|
await expect(changeStato(999999, 'PU', AUTH)).rejects.toMatchObject({ statusCode: 404 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("changeStato su un'attività di un altro autore lancia HttpError 403", async () => {
|
||||||
|
await save(baseAttivita(), MARIO);
|
||||||
|
const created = await prisma.attivita.findFirstOrThrow();
|
||||||
|
|
||||||
|
await expect(changeStato(created.id, 'PU', LUIGI)).rejects.toMatchObject({ statusCode: 403 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('changeStato aggiorna lo stato e lo restituisce come TipologicaDto', async () => {
|
||||||
|
await save(baseAttivita(), AUTH);
|
||||||
|
const created = await prisma.attivita.findFirstOrThrow();
|
||||||
|
|
||||||
|
const stato = await changeStato(created.id, 'PU', AUTH);
|
||||||
|
expect(stato).toEqual({ id: 'PU', nome: 'Pubblicato' });
|
||||||
|
|
||||||
|
const aggiornata = await prisma.attivita.findUniqueOrThrow({ where: { id: created.id } });
|
||||||
|
expect(aggiornata.statoId).toBe('PU');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getListaSearch combina filtri di gruppi diversi in AND e valori dello stesso gruppo in OR', async () => {
|
||||||
|
const lc = await prisma.branca.findFirstOrThrow({ where: { nome: 'L/C' } });
|
||||||
|
const gioco = await prisma.categoria.findFirstOrThrow({ where: { nome: 'gioco' } });
|
||||||
|
|
||||||
|
await save(
|
||||||
|
baseAttivita({
|
||||||
|
nome: 'L/C gioco',
|
||||||
|
brancaList: [{ nome: 'L/C' }],
|
||||||
|
categoriaList: [{ nome: 'gioco' }],
|
||||||
|
}),
|
||||||
|
AUTH,
|
||||||
|
);
|
||||||
|
await save(
|
||||||
|
baseAttivita({
|
||||||
|
nome: 'E/G gioco',
|
||||||
|
brancaList: [{ nome: 'E/G' }],
|
||||||
|
categoriaList: [{ nome: 'gioco' }],
|
||||||
|
}),
|
||||||
|
AUTH,
|
||||||
|
);
|
||||||
|
await save(
|
||||||
|
baseAttivita({
|
||||||
|
nome: 'L/C danza',
|
||||||
|
brancaList: [{ nome: 'L/C' }],
|
||||||
|
categoriaList: [{ nome: 'danza' }],
|
||||||
|
}),
|
||||||
|
AUTH,
|
||||||
|
);
|
||||||
|
|
||||||
|
const risultati = await getListaSearch([
|
||||||
|
{ gruppo: 'branca', id: lc.id, nome: null },
|
||||||
|
{ gruppo: 'categoria', id: gioco.id, nome: null },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(risultati).toHaveLength(1);
|
||||||
|
expect(risultati[0].nome).toBe('L/C gioco');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getListaSearch con filtro testo trova sia nel nome sia nei paragrafi', async () => {
|
||||||
|
await save(baseAttivita({ nome: 'Caccia al tesoro' }), AUTH);
|
||||||
|
await save(
|
||||||
|
baseAttivita({
|
||||||
|
nome: 'Attività generica',
|
||||||
|
paragrafoList: [
|
||||||
|
{
|
||||||
|
corpo: "C'è un tesoro nascosto nel bosco",
|
||||||
|
tipo: { id: 'PARAGRAFO' },
|
||||||
|
ordine: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
AUTH,
|
||||||
|
);
|
||||||
|
|
||||||
|
const risultati = await getListaSearch([{ gruppo: 'testo', nome: 'tesoro', id: null }]);
|
||||||
|
|
||||||
|
expect(risultati.map((r) => r.nome).sort()).toEqual(['Attività generica', 'Caccia al tesoro'].sort());
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { prisma } from '../../src/db/prisma';
|
||||||
|
import {
|
||||||
|
getBranca,
|
||||||
|
getCategoria,
|
||||||
|
getMateriale,
|
||||||
|
getPeriodoAnno,
|
||||||
|
getSearch,
|
||||||
|
} from '../../src/modules/autocomplete/autocomplete.service';
|
||||||
|
|
||||||
|
// Questo servizio legge solo le anagrafiche di base (branca, categoria,
|
||||||
|
// periodo_anno, materiale) gia' popolate dal seed: niente resetDatabase()
|
||||||
|
// completo qui, altrimenti perderemmo quei dati. L'unica tabella che questa
|
||||||
|
// suite potrebbe "sporcare" e' `materiale` (il seed non ne inserisce
|
||||||
|
// nessuno), quindi la puliamo prima di ogni test.
|
||||||
|
beforeEach(async () => {
|
||||||
|
await prisma.materiale.deleteMany();
|
||||||
|
});
|
||||||
|
|
||||||
|
function nomi(list: { nome: string | null }[]): string[] {
|
||||||
|
return list.map((item) => item.nome ?? '').sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('autocomplete.service', () => {
|
||||||
|
test('getBranca è case-insensitive e restituisce il gruppo "branca"', async () => {
|
||||||
|
const risultati = await getBranca('e/g');
|
||||||
|
|
||||||
|
expect(risultati).toHaveLength(1);
|
||||||
|
expect(risultati[0]).toMatchObject({ nome: 'E/G', gruppo: 'branca' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getCategoria("gioco") trova la categoria e le sue sotto-categorie che contengono "gioco"', async () => {
|
||||||
|
const risultati = await getCategoria('gioco');
|
||||||
|
|
||||||
|
expect(nomi(risultati)).toEqual(
|
||||||
|
['gioco', "gioco d'acqua", 'gioco giungla', 'gioco notturno', 'grande gioco'].sort(),
|
||||||
|
);
|
||||||
|
expect(risultati.every((r) => r.gruppo === 'categoria')).toBe(true);
|
||||||
|
expect(nomi(risultati)).not.toContain('torneo');
|
||||||
|
expect(nomi(risultati)).not.toContain('olimpiadi');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getPeriodoAnno("campo") trova i periodi "campo" ma non "promessa"', async () => {
|
||||||
|
const risultati = await getPeriodoAnno('campo');
|
||||||
|
|
||||||
|
expect(nomi(risultati)).toEqual(['campo estivo', 'campo invernale'].sort());
|
||||||
|
expect(risultati.every((r) => r.gruppo === 'periodoAnno')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getMateriale su un database senza materiali restituisce array vuoto', async () => {
|
||||||
|
const risultati = await getMateriale('corda');
|
||||||
|
|
||||||
|
expect(risultati).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getSearch(null) contiene sempre il gruppo Testo, popola Branca/Categoria/Periodo anno con tutte le righe e omette Materiale (vuoto)', async () => {
|
||||||
|
const gruppi = await getSearch(null);
|
||||||
|
|
||||||
|
expect(gruppi[0]).toEqual({
|
||||||
|
label: 'Testo',
|
||||||
|
objectsList: [{ id: null, nome: null, gruppo: 'testo' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const branca = gruppi.find((g) => g.label === 'Branca');
|
||||||
|
const categoria = gruppi.find((g) => g.label === 'Categoria');
|
||||||
|
const periodoAnno = gruppi.find((g) => g.label === 'Periodo anno');
|
||||||
|
const materiale = gruppi.find((g) => g.label === 'Materiale');
|
||||||
|
|
||||||
|
expect(branca?.objectsList).toHaveLength(await prisma.branca.count());
|
||||||
|
expect(categoria?.objectsList).toHaveLength(await prisma.categoria.count());
|
||||||
|
expect(periodoAnno?.objectsList).toHaveLength(await prisma.periodoAnno.count());
|
||||||
|
expect(materiale).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getSearch("gioco") restituisce Testo e Categoria ma non Branca, Periodo anno o Materiale', async () => {
|
||||||
|
const gruppi = await getSearch('gioco');
|
||||||
|
|
||||||
|
const labels = gruppi.map((g) => g.label);
|
||||||
|
expect(labels).toContain('Testo');
|
||||||
|
expect(labels).toContain('Categoria');
|
||||||
|
expect(labels).not.toContain('Branca');
|
||||||
|
expect(labels).not.toContain('Periodo anno');
|
||||||
|
expect(labels).not.toContain('Materiale');
|
||||||
|
|
||||||
|
const categoria = gruppi.find((g) => g.label === 'Categoria');
|
||||||
|
expect(nomi(categoria!.objectsList)).toEqual(
|
||||||
|
['gioco', "gioco d'acqua", 'gioco giungla', 'gioco notturno', 'grande gioco'].sort(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import request from 'supertest';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import nock from 'nock';
|
||||||
|
import { authenticate } from '../../../src/middlewares/authenticate';
|
||||||
|
import { bearer, mockKeycloakJwks, rogueKeyPem, signValidToken } from '../../setup/authTestHelper';
|
||||||
|
|
||||||
|
const CAPO_PAYLOAD = {
|
||||||
|
sub: 'user-123',
|
||||||
|
email: 'capo@example.com',
|
||||||
|
name: 'Capo Unità',
|
||||||
|
realm_access: { roles: ['capo-unita'] },
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildApp() {
|
||||||
|
const app = express();
|
||||||
|
app.get('/protetta', authenticate, (req, res) => {
|
||||||
|
res.json({ auth: req.auth });
|
||||||
|
});
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
mockKeycloakJwks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
nock.cleanAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('authenticate', () => {
|
||||||
|
test('restituisce 401 se manca il token', async () => {
|
||||||
|
const app = buildApp();
|
||||||
|
|
||||||
|
const response = await request(app).get('/protetta');
|
||||||
|
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('restituisce 401 se il token è scaduto', async () => {
|
||||||
|
const app = buildApp();
|
||||||
|
const token = signValidToken(CAPO_PAYLOAD, { expiresIn: '-10s' });
|
||||||
|
|
||||||
|
const response = await request(app).get('/protetta').set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('restituisce 401 se il token ha una firma non valida', async () => {
|
||||||
|
const app = buildApp();
|
||||||
|
const token = jwt.sign(CAPO_PAYLOAD, rogueKeyPem, {
|
||||||
|
algorithm: 'RS256',
|
||||||
|
keyid: 'test-kid',
|
||||||
|
expiresIn: '5m',
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app).get('/protetta').set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('restituisce 200 e popola req.auth se il token è valido', async () => {
|
||||||
|
const app = buildApp();
|
||||||
|
|
||||||
|
const response = await request(app).get('/protetta').set('Authorization', bearer(CAPO_PAYLOAD));
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.auth).toEqual({
|
||||||
|
userId: 'user-123',
|
||||||
|
email: 'capo@example.com',
|
||||||
|
name: 'Capo Unità',
|
||||||
|
roles: ['capo-unita'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": ".",
|
||||||
|
"noEmit": true,
|
||||||
|
"declaration": false
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "tests/**/*.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2021",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"lib": ["ES2021"],
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"declaration": false,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist", "tests"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user