Aggiunge infrastruttura PocketBase locale (Docker, schema, hook)
Introduce PocketBase come sostituto di Supabase solo per l'ambiente di sviluppo locale (docs/PORTABILITA.md): docker-compose.pocketbase.yml, schema iniziale in pocketbase/pb_migrations/ (collection equivalenti alle tabelle Supabase, con le stesse API rule dove esprimibili) e la logica procedurale che in Postgres viveva in trigger/funzioni riscritta come hook JS in pocketbase/pb_hooks/ (claim slot giocatore, blocco autovoto, convocati/pagelle chiuse, immutabilità di risposto_il). Il superuser e il provider Google OAuth2 si ricreano da soli a ogni avvio da variabili d'ambiente (script scripts/pocketbase-reset.sh), così un reset non richiede passaggi manuali da dashboard. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+5
-1
@@ -40,4 +40,8 @@ dist-ssr
|
|||||||
# Optional
|
# Optional
|
||||||
.vercel
|
.vercel
|
||||||
# Supabase CLI local state
|
# Supabase CLI local state
|
||||||
supabase/.temp/
|
supabase/.temp/
|
||||||
|
|
||||||
|
# PocketBase locale (dati e log runtime, non lo schema in pb_migrations/pb_hooks)
|
||||||
|
pocketbase/pb_data/*
|
||||||
|
!pocketbase/pb_data/.gitkeep
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# PocketBase locale per lo sviluppo (sostituisce "npx supabase start" solo in locale,
|
||||||
|
# vedi docs/PORTABILITA.md e la voce DD relativa). La produzione resta su Supabase.
|
||||||
|
#
|
||||||
|
# Uso:
|
||||||
|
# docker compose -f docker-compose.pocketbase.yml up -d # avvia
|
||||||
|
# docker compose -f docker-compose.pocketbase.yml down # ferma
|
||||||
|
# docker compose -f docker-compose.pocketbase.yml down -v # ferma e azzera i dati locali
|
||||||
|
#
|
||||||
|
# Dashboard admin: http://127.0.0.1:8090/_/
|
||||||
|
# API: http://127.0.0.1:8090/api/
|
||||||
|
|
||||||
|
services:
|
||||||
|
pocketbase:
|
||||||
|
image: ghcr.io/muchobien/pocketbase:latest
|
||||||
|
container_name: crapp-pocketbase
|
||||||
|
restart: unless-stopped
|
||||||
|
# --automigrate=0: le modifiche fatte da dashboard (es. abilitare un provider OAuth2,
|
||||||
|
# con relativo client secret) NON devono finire come file di migration versionati in
|
||||||
|
# git. Le migration restano solo quelle scritte a mano in pocketbase/pb_migrations/.
|
||||||
|
command: ["--automigrate=0"]
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
# L'immagine crea il superuser da queste variabili a ogni avvio (idempotente):
|
||||||
|
# sopravvive a "npm run pocketbase:reset" senza passaggi manuali.
|
||||||
|
PB_ADMIN_EMAIL: ${POCKETBASE_SUPERUSER_EMAIL:-}
|
||||||
|
PB_ADMIN_PASSWORD: ${POCKETBASE_SUPERUSER_PASSWORD:-}
|
||||||
|
ports:
|
||||||
|
- "8090:8090"
|
||||||
|
volumes:
|
||||||
|
- ./pocketbase/pb_data:/pb_data
|
||||||
|
- ./pocketbase/pb_migrations:/pb_migrations
|
||||||
|
- ./pocketbase/pb_hooks:/pb_hooks
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:8090/api/health"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
+1
-1
@@ -6,7 +6,7 @@ import reactRefresh from "eslint-plugin-react-refresh";
|
|||||||
import tseslint from "typescript-eslint";
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
{ ignores: ["dist", ".output", ".vinxi"] },
|
{ ignores: ["dist", ".output", ".vinxi", "pocketbase/pb_data"] },
|
||||||
{
|
{
|
||||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
files: ["**/*.{ts,tsx}"],
|
files: ["**/*.{ts,tsx}"],
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
//
|
||||||
|
// Equivalente del trigger enforce_giocatori_squadra_update (M1/M5/M8, DD-016/DD-018):
|
||||||
|
// un non-admin può aggiornare uno slot di giocatori_squadra solo per reclamarlo (slot
|
||||||
|
// libero, email dell'account uguale a quella registrata sulla riga), senza cambiare nessun
|
||||||
|
// altro campo. La updateRule della collection apre la porta solo quando auth_user_id è
|
||||||
|
// vuoto o l'utente è admin; qui si valida il resto.
|
||||||
|
|
||||||
|
onRecordUpdateRequest((e) => {
|
||||||
|
// Superuser (pbAdmin lato server, dashboard, script di migrazione dati) e admin
|
||||||
|
// applicativi (role="admin" sulla collection users) bypassano il vincolo di claim.
|
||||||
|
const isAdmin = e.hasSuperuserAuth() || (e.auth && e.auth.get("role") === "admin");
|
||||||
|
if (isAdmin) {
|
||||||
|
e.next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originale = e.app.findRecordById("giocatori_squadra", e.record.id);
|
||||||
|
|
||||||
|
const campiBloccati = [
|
||||||
|
"nome",
|
||||||
|
"cognome",
|
||||||
|
"numero",
|
||||||
|
"ruolo",
|
||||||
|
"attivo",
|
||||||
|
"email",
|
||||||
|
"numero_tessera",
|
||||||
|
"data_tessera",
|
||||||
|
];
|
||||||
|
for (const campo of campiBloccati) {
|
||||||
|
if (String(e.record.get(campo)) !== String(originale.get(campo))) {
|
||||||
|
throw new BadRequestError("Aggiornamento non autorizzato su giocatori_squadra");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const utenteId = e.auth ? e.auth.id : "";
|
||||||
|
const emailAccount = (e.auth ? e.auth.get("email") || "" : "").toLowerCase();
|
||||||
|
const emailSlot = (originale.get("email") || "").toLowerCase();
|
||||||
|
|
||||||
|
const slotLibero = !originale.get("auth_user_id");
|
||||||
|
const claimSuAccountCorrente = e.record.get("auth_user_id") === utenteId;
|
||||||
|
const emailCombacia = emailSlot !== "" && emailSlot === emailAccount;
|
||||||
|
|
||||||
|
if (!slotLibero || !claimSuAccountCorrente || !emailCombacia) {
|
||||||
|
throw new BadRequestError("Aggiornamento non autorizzato su giocatori_squadra");
|
||||||
|
}
|
||||||
|
|
||||||
|
e.next();
|
||||||
|
}, "giocatori_squadra");
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
//
|
||||||
|
// Il Client ID/Secret di Google OAuth2 configurati da dashboard vivono in pb_data e
|
||||||
|
// andrebbero persi a ogni "npm run pocketbase:reset" (azzera pb_data). Questo hook li
|
||||||
|
// reimposta a ogni avvio da variabili d'ambiente (mai committate, solo in .env), così
|
||||||
|
// sopravvivono al reset senza reinserirli a mano ogni volta dalla dashboard.
|
||||||
|
|
||||||
|
onBootstrap((e) => {
|
||||||
|
e.next();
|
||||||
|
|
||||||
|
const clientId = $os.getenv("GOOGLE_OAUTH_CLIENT_ID");
|
||||||
|
const clientSecret = $os.getenv("GOOGLE_OAUTH_CLIENT_SECRET");
|
||||||
|
if (!clientId || !clientSecret) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const collection = e.app.findCollectionByNameOrId("users");
|
||||||
|
collection.oauth2.enabled = true;
|
||||||
|
collection.oauth2.providers = [
|
||||||
|
{ name: "google", clientId: clientId, clientSecret: clientSecret },
|
||||||
|
];
|
||||||
|
e.app.save(collection);
|
||||||
|
});
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
//
|
||||||
|
// Equivalente del trigger risposte_presenze_risposto_il_immutabile (M9): risposto_il è
|
||||||
|
// l'istante della PRIMA risposta, scritto una sola volta e mai più modificabile — serve
|
||||||
|
// per la serie "Conferme 24h" (confronto con eventi_app.created).
|
||||||
|
|
||||||
|
onRecordCreateRequest((e) => {
|
||||||
|
e.record.set("risposto_il", new Date().toISOString());
|
||||||
|
e.next();
|
||||||
|
}, "risposte_presenze");
|
||||||
|
|
||||||
|
onRecordUpdateRequest((e) => {
|
||||||
|
const originale = e.app.findRecordById("risposte_presenze", e.record.id);
|
||||||
|
e.record.set("risposto_il", originale.get("risposto_il"));
|
||||||
|
e.next();
|
||||||
|
}, "risposte_presenze");
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
//
|
||||||
|
// Equivalente di evento_permette_voto() + i CHECK "no autovoto" (M12/M13): applicato a
|
||||||
|
// mvp_voti, pagelle_voti, badge_social_voti su create/update. Gli admin non hanno questi
|
||||||
|
// vincoli (possono correggere un voto anche fuori convocazione/dopo la chiusura pagelle).
|
||||||
|
// convocati vuoto = "tutta la rosa" (stessa convenzione di convocatiEvento() in eventi.ts).
|
||||||
|
//
|
||||||
|
// La validazione è ripetuta in ciascun callback (non estratta in una funzione condivisa a
|
||||||
|
// livello di file): ogni hook di PocketBase viene eseguito nel proprio contesto JS isolato,
|
||||||
|
// che non vede le funzioni dichiarate fuori dal callback stesso (verificato empiricamente:
|
||||||
|
// una funzione condivisa causa "ReferenceError: ... is not defined" a runtime).
|
||||||
|
|
||||||
|
onRecordCreateRequest((e) => {
|
||||||
|
const isAdmin = e.hasSuperuserAuth() || (e.auth && e.auth.get("role") === "admin");
|
||||||
|
if (!isAdmin) {
|
||||||
|
const votanteId = e.record.get("votante");
|
||||||
|
const votatoId = e.record.get("votato");
|
||||||
|
if (votanteId === votatoId) {
|
||||||
|
throw new BadRequestError("Non puoi votare te stesso");
|
||||||
|
}
|
||||||
|
const evento = e.app.findRecordById("eventi_app", e.record.get("evento"));
|
||||||
|
const convocati = evento.get("convocati") || [];
|
||||||
|
if (
|
||||||
|
convocati.length > 0 &&
|
||||||
|
(convocati.indexOf(votanteId) === -1 || convocati.indexOf(votatoId) === -1)
|
||||||
|
) {
|
||||||
|
throw new BadRequestError("Votante e votato devono essere convocati all'evento");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.next();
|
||||||
|
}, "mvp_voti");
|
||||||
|
|
||||||
|
onRecordUpdateRequest((e) => {
|
||||||
|
const isAdmin = e.hasSuperuserAuth() || (e.auth && e.auth.get("role") === "admin");
|
||||||
|
if (!isAdmin) {
|
||||||
|
const votanteId = e.record.get("votante");
|
||||||
|
const votatoId = e.record.get("votato");
|
||||||
|
if (votanteId === votatoId) {
|
||||||
|
throw new BadRequestError("Non puoi votare te stesso");
|
||||||
|
}
|
||||||
|
const evento = e.app.findRecordById("eventi_app", e.record.get("evento"));
|
||||||
|
const convocati = evento.get("convocati") || [];
|
||||||
|
if (
|
||||||
|
convocati.length > 0 &&
|
||||||
|
(convocati.indexOf(votanteId) === -1 || convocati.indexOf(votatoId) === -1)
|
||||||
|
) {
|
||||||
|
throw new BadRequestError("Votante e votato devono essere convocati all'evento");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.next();
|
||||||
|
}, "mvp_voti");
|
||||||
|
|
||||||
|
onRecordCreateRequest((e) => {
|
||||||
|
const isAdmin = e.hasSuperuserAuth() || (e.auth && e.auth.get("role") === "admin");
|
||||||
|
if (!isAdmin) {
|
||||||
|
const votanteId = e.record.get("votante");
|
||||||
|
const votatoId = e.record.get("votato");
|
||||||
|
if (votanteId === votatoId) {
|
||||||
|
throw new BadRequestError("Non puoi votare te stesso");
|
||||||
|
}
|
||||||
|
const evento = e.app.findRecordById("eventi_app", e.record.get("evento"));
|
||||||
|
const convocati = evento.get("convocati") || [];
|
||||||
|
if (
|
||||||
|
convocati.length > 0 &&
|
||||||
|
(convocati.indexOf(votanteId) === -1 || convocati.indexOf(votatoId) === -1)
|
||||||
|
) {
|
||||||
|
throw new BadRequestError("Votante e votato devono essere convocati all'evento");
|
||||||
|
}
|
||||||
|
if (evento.get("pagelle_chiuse")) {
|
||||||
|
throw new BadRequestError("Le pagelle per questo evento sono chiuse");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.next();
|
||||||
|
}, "pagelle_voti");
|
||||||
|
|
||||||
|
onRecordUpdateRequest((e) => {
|
||||||
|
const isAdmin = e.hasSuperuserAuth() || (e.auth && e.auth.get("role") === "admin");
|
||||||
|
if (!isAdmin) {
|
||||||
|
const votanteId = e.record.get("votante");
|
||||||
|
const votatoId = e.record.get("votato");
|
||||||
|
if (votanteId === votatoId) {
|
||||||
|
throw new BadRequestError("Non puoi votare te stesso");
|
||||||
|
}
|
||||||
|
const evento = e.app.findRecordById("eventi_app", e.record.get("evento"));
|
||||||
|
const convocati = evento.get("convocati") || [];
|
||||||
|
if (
|
||||||
|
convocati.length > 0 &&
|
||||||
|
(convocati.indexOf(votanteId) === -1 || convocati.indexOf(votatoId) === -1)
|
||||||
|
) {
|
||||||
|
throw new BadRequestError("Votante e votato devono essere convocati all'evento");
|
||||||
|
}
|
||||||
|
if (evento.get("pagelle_chiuse")) {
|
||||||
|
throw new BadRequestError("Le pagelle per questo evento sono chiuse");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.next();
|
||||||
|
}, "pagelle_voti");
|
||||||
|
|
||||||
|
onRecordCreateRequest((e) => {
|
||||||
|
const isAdmin = e.hasSuperuserAuth() || (e.auth && e.auth.get("role") === "admin");
|
||||||
|
if (!isAdmin) {
|
||||||
|
const votanteId = e.record.get("votante");
|
||||||
|
const votatoId = e.record.get("votato");
|
||||||
|
if (votanteId === votatoId) {
|
||||||
|
throw new BadRequestError("Non puoi votare te stesso");
|
||||||
|
}
|
||||||
|
const evento = e.app.findRecordById("eventi_app", e.record.get("evento"));
|
||||||
|
const convocati = evento.get("convocati") || [];
|
||||||
|
if (
|
||||||
|
convocati.length > 0 &&
|
||||||
|
(convocati.indexOf(votanteId) === -1 || convocati.indexOf(votatoId) === -1)
|
||||||
|
) {
|
||||||
|
throw new BadRequestError("Votante e votato devono essere convocati all'evento");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.next();
|
||||||
|
}, "badge_social_voti");
|
||||||
|
|
||||||
|
onRecordUpdateRequest((e) => {
|
||||||
|
const isAdmin = e.hasSuperuserAuth() || (e.auth && e.auth.get("role") === "admin");
|
||||||
|
if (!isAdmin) {
|
||||||
|
const votanteId = e.record.get("votante");
|
||||||
|
const votatoId = e.record.get("votato");
|
||||||
|
if (votanteId === votatoId) {
|
||||||
|
throw new BadRequestError("Non puoi votare te stesso");
|
||||||
|
}
|
||||||
|
const evento = e.app.findRecordById("eventi_app", e.record.get("evento"));
|
||||||
|
const convocati = evento.get("convocati") || [];
|
||||||
|
if (
|
||||||
|
convocati.length > 0 &&
|
||||||
|
(convocati.indexOf(votanteId) === -1 || convocati.indexOf(votatoId) === -1)
|
||||||
|
) {
|
||||||
|
throw new BadRequestError("Votante e votato devono essere convocati all'evento");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.next();
|
||||||
|
}, "badge_social_voti");
|
||||||
@@ -0,0 +1,665 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
//
|
||||||
|
// Schema iniziale PocketBase per CrAPP — solo ambiente locale (Docker).
|
||||||
|
//
|
||||||
|
// Non è la riproduzione 1:1 delle 27 migration Supabase in supabase/migrations/: rappresenta
|
||||||
|
// lo STATO FINALE attuale dello schema (fonte: docs/DATABASE.md + lettura diretta delle
|
||||||
|
// migration Supabase più recenti), riscritto nel modello PocketBase (collection + campi +
|
||||||
|
// API rules). Le tabelle Supabase non più usate dal codice (`giocatori`, `eventi`/`presenze`
|
||||||
|
// "nuovo modello" mai adottato, `promemoria_push` deprecata) non vengono portate.
|
||||||
|
//
|
||||||
|
// `user_roles` non diventa una collection: il ruolo admin/user è un campo diretto sulla
|
||||||
|
// collection auth nativa `users` (decisione 3 del piano di migrazione).
|
||||||
|
//
|
||||||
|
// La logica procedurale che in Postgres viveva in trigger/funzioni (claim-slot su
|
||||||
|
// giocatori_squadra, blocco autovoto, convocati/pagelle_chiuse, cascata su cancellazione
|
||||||
|
// evento, risposto_il immutabile) NON sta qui: è in pb_hooks/*.pb.js, perché le API rules di
|
||||||
|
// PocketBase sono espressioni, non codice procedurale.
|
||||||
|
|
||||||
|
migrate(
|
||||||
|
(app) => {
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// users (collection auth nativa): aggiunge il campo ruolo che sostituisce user_roles.
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
const users = app.findCollectionByNameOrId("users");
|
||||||
|
users.fields.add(
|
||||||
|
new Field({
|
||||||
|
name: "role",
|
||||||
|
type: "select",
|
||||||
|
required: true,
|
||||||
|
values: ["user", "admin"],
|
||||||
|
maxSelect: 1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// Ogni utente autenticato legge il proprio record; l'admin (verificato lato hook al primo
|
||||||
|
// login, poi qui) può leggere tutti. Il ruolo lo scrive solo chi è già admin.
|
||||||
|
users.listRule = "id = @request.auth.id || @request.auth.role = 'admin'";
|
||||||
|
users.viewRule = "id = @request.auth.id || @request.auth.role = 'admin'";
|
||||||
|
users.updateRule = "id = @request.auth.id";
|
||||||
|
app.save(users);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// giocatori_squadra — anagrafica operativa della squadra (DD-015, DD-016, DD-018)
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
const giocatori = new Collection({
|
||||||
|
name: "giocatori_squadra",
|
||||||
|
type: "base",
|
||||||
|
fields: [
|
||||||
|
// Id testuali corti (g1..gN, come in Supabase): il vincolo di default di PocketBase
|
||||||
|
// (minimo 15 caratteri) va rilassato esplicitamente.
|
||||||
|
{ name: "id", type: "text", min: 1, max: 40, pattern: "^g[0-9]+$" },
|
||||||
|
{ name: "nome", type: "text", required: true },
|
||||||
|
{ name: "cognome", type: "text", required: true },
|
||||||
|
{ name: "numero", type: "number", required: true, min: 1 },
|
||||||
|
{ name: "ruolo", type: "text", required: true },
|
||||||
|
{
|
||||||
|
name: "auth_user_id",
|
||||||
|
type: "relation",
|
||||||
|
required: false,
|
||||||
|
collectionId: users.id,
|
||||||
|
maxSelect: 1,
|
||||||
|
},
|
||||||
|
{ name: "email", type: "email", required: false },
|
||||||
|
{ name: "attivo", type: "bool", required: false },
|
||||||
|
{ name: "numero_tessera", type: "text", required: false },
|
||||||
|
{ name: "data_tessera", type: "date", required: false },
|
||||||
|
{ name: "created", type: "autodate", onCreate: true },
|
||||||
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
"CREATE UNIQUE INDEX idx_giocatori_squadra_auth_user ON giocatori_squadra (auth_user_id) WHERE auth_user_id != ''",
|
||||||
|
"CREATE UNIQUE INDEX idx_giocatori_squadra_email ON giocatori_squadra (email) WHERE email != ''",
|
||||||
|
],
|
||||||
|
// Lettura: rosa attiva a tutti gli autenticati, tutta la rosa agli admin (DD-015/M1).
|
||||||
|
listRule: "@request.auth.id != '' && (attivo = true || @request.auth.role = 'admin')",
|
||||||
|
viewRule: "@request.auth.id != '' && (attivo = true || @request.auth.role = 'admin')",
|
||||||
|
// Creazione slot: solo admin.
|
||||||
|
createRule: "@request.auth.role = 'admin'",
|
||||||
|
// Aggiornamento: admin senza vincoli, oppure claim di uno slot libero — il resto
|
||||||
|
// (che il claim tocchi solo auth_user_id e rispetti l'email) lo valida l'hook
|
||||||
|
// giocatori_squadra_claim.pb.js, la rule qui apre solo la porta.
|
||||||
|
updateRule: "@request.auth.role = 'admin' || (@request.auth.id != '' && auth_user_id = '')",
|
||||||
|
deleteRule: "@request.auth.role = 'admin'",
|
||||||
|
});
|
||||||
|
app.save(giocatori);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// profili_giocatore — dati personali/documento/certificato, 1:1 con giocatori_squadra
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
const profili = new Collection({
|
||||||
|
name: "profili_giocatore",
|
||||||
|
type: "base",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "giocatore",
|
||||||
|
type: "relation",
|
||||||
|
required: true,
|
||||||
|
collectionId: giocatori.id,
|
||||||
|
maxSelect: 1,
|
||||||
|
cascadeDelete: true,
|
||||||
|
},
|
||||||
|
{ name: "data_nascita", type: "date", required: false },
|
||||||
|
{ name: "luogo_nascita", type: "text", required: false },
|
||||||
|
{ name: "indirizzo", type: "text", required: false },
|
||||||
|
{ name: "telefono", type: "text", required: false },
|
||||||
|
{ name: "email", type: "email", required: false },
|
||||||
|
{
|
||||||
|
name: "documento_tipo",
|
||||||
|
type: "select",
|
||||||
|
required: false,
|
||||||
|
maxSelect: 1,
|
||||||
|
values: ["Carta d'identità", "Passaporto", "Patente"],
|
||||||
|
},
|
||||||
|
{ name: "documento_numero", type: "text", required: false },
|
||||||
|
{ name: "documento_rilasciato_da", type: "text", required: false },
|
||||||
|
{ name: "documento_emissione", type: "date", required: false },
|
||||||
|
{ name: "documento_scadenza", type: "date", required: false },
|
||||||
|
{
|
||||||
|
name: "documento_fronte",
|
||||||
|
type: "file",
|
||||||
|
required: false,
|
||||||
|
protected: true,
|
||||||
|
maxSelect: 1,
|
||||||
|
maxSize: 10485760,
|
||||||
|
mimeTypes: ["image/jpeg", "image/png", "image/webp", "application/pdf"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "documento_retro",
|
||||||
|
type: "file",
|
||||||
|
required: false,
|
||||||
|
protected: true,
|
||||||
|
maxSelect: 1,
|
||||||
|
maxSize: 10485760,
|
||||||
|
mimeTypes: ["image/jpeg", "image/png", "image/webp", "application/pdf"],
|
||||||
|
},
|
||||||
|
{ name: "certificato_scadenza", type: "date", required: false },
|
||||||
|
{
|
||||||
|
name: "certificato",
|
||||||
|
type: "file",
|
||||||
|
required: false,
|
||||||
|
protected: true,
|
||||||
|
maxSelect: 1,
|
||||||
|
maxSize: 10485760,
|
||||||
|
mimeTypes: ["image/jpeg", "image/png", "image/webp", "application/pdf"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "foto",
|
||||||
|
type: "file",
|
||||||
|
required: false,
|
||||||
|
protected: true,
|
||||||
|
maxSelect: 1,
|
||||||
|
maxSize: 10485760,
|
||||||
|
mimeTypes: ["image/jpeg", "image/png", "image/webp"],
|
||||||
|
},
|
||||||
|
{ name: "created", type: "autodate", onCreate: true },
|
||||||
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
"CREATE UNIQUE INDEX idx_profili_giocatore_giocatore ON profili_giocatore (giocatore)",
|
||||||
|
],
|
||||||
|
// Documenti sanitari/identità: mai pubblici (DD-016 regola 4). Solo il giocatore
|
||||||
|
// proprietario o un admin, sia per i campi sia per i file allegati (le API rules
|
||||||
|
// governano anche il download dei file in PocketBase, non serve un bucket a parte).
|
||||||
|
listRule:
|
||||||
|
"@request.auth.id != '' && (giocatore.auth_user_id = @request.auth.id || @request.auth.role = 'admin')",
|
||||||
|
viewRule:
|
||||||
|
"@request.auth.id != '' && (giocatore.auth_user_id = @request.auth.id || @request.auth.role = 'admin')",
|
||||||
|
createRule:
|
||||||
|
"@request.auth.id != '' && (giocatore.auth_user_id = @request.auth.id || @request.auth.role = 'admin')",
|
||||||
|
updateRule:
|
||||||
|
"@request.auth.id != '' && (giocatore.auth_user_id = @request.auth.id || @request.auth.role = 'admin')",
|
||||||
|
deleteRule: "@request.auth.role = 'admin'",
|
||||||
|
});
|
||||||
|
app.save(profili);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// avatar_giocatori — foto profilo pubbliche (M6 avatar-giocatori). Collection separata
|
||||||
|
// da giocatori_squadra: la policy è "chiunque autenticato carica/sostituisce/elimina
|
||||||
|
// qualsiasi avatar" (nessun controllo per-proprietario, DD in docs/DATABASE.md), molto
|
||||||
|
// più permissiva delle regole di giocatori_squadra — mescolarle avrebbe o indebolito
|
||||||
|
// quelle o impedito il caricamento a chi non è admin.
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
const avatar = new Collection({
|
||||||
|
name: "avatar_giocatori",
|
||||||
|
type: "base",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "giocatore",
|
||||||
|
type: "relation",
|
||||||
|
required: true,
|
||||||
|
collectionId: giocatori.id,
|
||||||
|
maxSelect: 1,
|
||||||
|
cascadeDelete: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "foto",
|
||||||
|
type: "file",
|
||||||
|
required: false,
|
||||||
|
maxSelect: 1,
|
||||||
|
maxSize: 5242880,
|
||||||
|
mimeTypes: ["image/jpeg"],
|
||||||
|
},
|
||||||
|
{ name: "created", type: "autodate", onCreate: true },
|
||||||
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
"CREATE UNIQUE INDEX idx_avatar_giocatori_giocatore ON avatar_giocatori (giocatore)",
|
||||||
|
],
|
||||||
|
// Bucket pubblico in Supabase: leggibile da chiunque, anche senza login.
|
||||||
|
listRule: "",
|
||||||
|
viewRule: "",
|
||||||
|
createRule: "@request.auth.id != ''",
|
||||||
|
updateRule: "@request.auth.id != ''",
|
||||||
|
deleteRule: "@request.auth.id != ''",
|
||||||
|
});
|
||||||
|
app.save(avatar);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// eventi_app — eventi gestionali (partite/allenamenti/eventi squadra)
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
const eventi = new Collection({
|
||||||
|
name: "eventi_app",
|
||||||
|
type: "base",
|
||||||
|
fields: [
|
||||||
|
// Id testuali corti (e1..eN, come in Supabase).
|
||||||
|
{ name: "id", type: "text", min: 1, max: 40, pattern: "^e[0-9]+$" },
|
||||||
|
{
|
||||||
|
name: "tipo",
|
||||||
|
type: "select",
|
||||||
|
required: true,
|
||||||
|
maxSelect: 1,
|
||||||
|
values: ["partita", "allenamento", "evento"],
|
||||||
|
},
|
||||||
|
{ name: "titolo", type: "text", required: true },
|
||||||
|
{ name: "luogo", type: "text", required: false },
|
||||||
|
{ name: "data", type: "date", required: true },
|
||||||
|
{ name: "ora", type: "text", required: false },
|
||||||
|
{ name: "note", type: "text", required: false },
|
||||||
|
{
|
||||||
|
name: "convocati",
|
||||||
|
type: "relation",
|
||||||
|
required: false,
|
||||||
|
collectionId: giocatori.id,
|
||||||
|
maxSelect: 999,
|
||||||
|
},
|
||||||
|
{ name: "campionato", type: "bool", required: false },
|
||||||
|
{ name: "casa", type: "bool", required: false },
|
||||||
|
{ name: "pagelle_chiuse", type: "bool", required: false },
|
||||||
|
{ name: "created", type: "autodate", onCreate: true },
|
||||||
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
||||||
|
],
|
||||||
|
// Lettura aperta a tutti gli autenticati; scrittura solo admin (rotta /eventi già
|
||||||
|
// riservata — M11/DD-023).
|
||||||
|
listRule: "@request.auth.id != ''",
|
||||||
|
viewRule: "@request.auth.id != ''",
|
||||||
|
createRule: "@request.auth.role = 'admin'",
|
||||||
|
updateRule: "@request.auth.role = 'admin'",
|
||||||
|
deleteRule: "@request.auth.role = 'admin'",
|
||||||
|
});
|
||||||
|
app.save(eventi);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// risposte_presenze — risposte dei giocatori agli eventi
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
const presenze = new Collection({
|
||||||
|
name: "risposte_presenze",
|
||||||
|
type: "base",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "evento",
|
||||||
|
type: "relation",
|
||||||
|
required: true,
|
||||||
|
collectionId: eventi.id,
|
||||||
|
maxSelect: 1,
|
||||||
|
cascadeDelete: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "giocatore",
|
||||||
|
type: "relation",
|
||||||
|
required: true,
|
||||||
|
collectionId: giocatori.id,
|
||||||
|
maxSelect: 1,
|
||||||
|
},
|
||||||
|
{ name: "stato", type: "text", required: true },
|
||||||
|
// Istante della PRIMA risposta (M9): valorizzato e reso immutabile dall'hook
|
||||||
|
// risposte_presenze_immutabili.pb.js, non scrivibile direttamente dal client.
|
||||||
|
{ name: "risposto_il", type: "date", required: false },
|
||||||
|
{ name: "created", type: "autodate", onCreate: true },
|
||||||
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
"CREATE UNIQUE INDEX idx_risposte_presenze_evento_giocatore ON risposte_presenze (evento, giocatore)",
|
||||||
|
],
|
||||||
|
listRule: "@request.auth.id != ''",
|
||||||
|
viewRule: "@request.auth.id != ''",
|
||||||
|
createRule:
|
||||||
|
"@request.auth.role = 'admin' || (@request.auth.id != '' && giocatore.auth_user_id = @request.auth.id)",
|
||||||
|
updateRule:
|
||||||
|
"@request.auth.role = 'admin' || (@request.auth.id != '' && giocatore.auth_user_id = @request.auth.id)",
|
||||||
|
deleteRule:
|
||||||
|
"@request.auth.role = 'admin' || (@request.auth.id != '' && giocatore.auth_user_id = @request.auth.id)",
|
||||||
|
});
|
||||||
|
app.save(presenze);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// cacche_partita — sondaggio pre-partita (statistiche/badge segreti)
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
const cacche = new Collection({
|
||||||
|
name: "cacche_partita",
|
||||||
|
type: "base",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "evento",
|
||||||
|
type: "relation",
|
||||||
|
required: true,
|
||||||
|
collectionId: eventi.id,
|
||||||
|
maxSelect: 1,
|
||||||
|
cascadeDelete: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "giocatore",
|
||||||
|
type: "relation",
|
||||||
|
required: true,
|
||||||
|
collectionId: giocatori.id,
|
||||||
|
maxSelect: 1,
|
||||||
|
},
|
||||||
|
{ name: "quantita", type: "number", required: true, min: 0, max: 10 },
|
||||||
|
{ name: "created", type: "autodate", onCreate: true },
|
||||||
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
"CREATE UNIQUE INDEX idx_cacche_partita_evento_giocatore ON cacche_partita (evento, giocatore)",
|
||||||
|
],
|
||||||
|
listRule: "@request.auth.id != ''",
|
||||||
|
viewRule: "@request.auth.id != ''",
|
||||||
|
createRule:
|
||||||
|
"@request.auth.role = 'admin' || (@request.auth.id != '' && giocatore.auth_user_id = @request.auth.id)",
|
||||||
|
updateRule:
|
||||||
|
"@request.auth.role = 'admin' || (@request.auth.id != '' && giocatore.auth_user_id = @request.auth.id)",
|
||||||
|
deleteRule:
|
||||||
|
"@request.auth.role = 'admin' || (@request.auth.id != '' && giocatore.auth_user_id = @request.auth.id)",
|
||||||
|
});
|
||||||
|
app.save(cacche);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// mvp_voti / pagelle_voti / badge_social_voti — votazioni tra compagni
|
||||||
|
//
|
||||||
|
// Ownership (votante = chi scrive) è espressa qui via API rule. Autovoto e vincoli
|
||||||
|
// "convocato all'evento" / "pagelle non chiuse" (M12/M13) NON sono esprimibili in modo
|
||||||
|
// sicuro come semplice espressione di rule su relazioni multiple: li applica l'hook
|
||||||
|
// pb_hooks/voti_convocati.pb.js su create/update, per tutte e tre le collection.
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
const mvpVoti = new Collection({
|
||||||
|
name: "mvp_voti",
|
||||||
|
type: "base",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "evento",
|
||||||
|
type: "relation",
|
||||||
|
required: true,
|
||||||
|
collectionId: eventi.id,
|
||||||
|
maxSelect: 1,
|
||||||
|
cascadeDelete: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "votante",
|
||||||
|
type: "relation",
|
||||||
|
required: true,
|
||||||
|
collectionId: giocatori.id,
|
||||||
|
maxSelect: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "votato",
|
||||||
|
type: "relation",
|
||||||
|
required: true,
|
||||||
|
collectionId: giocatori.id,
|
||||||
|
maxSelect: 1,
|
||||||
|
},
|
||||||
|
{ name: "votato_nome", type: "text", required: true },
|
||||||
|
{ name: "created", type: "autodate", onCreate: true },
|
||||||
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
||||||
|
],
|
||||||
|
indexes: ["CREATE UNIQUE INDEX idx_mvp_voti_evento_votante ON mvp_voti (evento, votante)"],
|
||||||
|
listRule: "@request.auth.id != ''",
|
||||||
|
viewRule: "@request.auth.id != ''",
|
||||||
|
createRule:
|
||||||
|
"@request.auth.role = 'admin' || (@request.auth.id != '' && votante.auth_user_id = @request.auth.id)",
|
||||||
|
updateRule:
|
||||||
|
"@request.auth.role = 'admin' || (@request.auth.id != '' && votante.auth_user_id = @request.auth.id)",
|
||||||
|
deleteRule:
|
||||||
|
"@request.auth.role = 'admin' || (@request.auth.id != '' && votante.auth_user_id = @request.auth.id)",
|
||||||
|
});
|
||||||
|
app.save(mvpVoti);
|
||||||
|
|
||||||
|
const pagelleVoti = new Collection({
|
||||||
|
name: "pagelle_voti",
|
||||||
|
type: "base",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "evento",
|
||||||
|
type: "relation",
|
||||||
|
required: true,
|
||||||
|
collectionId: eventi.id,
|
||||||
|
maxSelect: 1,
|
||||||
|
cascadeDelete: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "votante",
|
||||||
|
type: "relation",
|
||||||
|
required: true,
|
||||||
|
collectionId: giocatori.id,
|
||||||
|
maxSelect: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "votato",
|
||||||
|
type: "relation",
|
||||||
|
required: true,
|
||||||
|
collectionId: giocatori.id,
|
||||||
|
maxSelect: 1,
|
||||||
|
},
|
||||||
|
{ name: "voto", type: "number", required: true, min: 1, max: 10 },
|
||||||
|
{ name: "created", type: "autodate", onCreate: true },
|
||||||
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
"CREATE UNIQUE INDEX idx_pagelle_voti_evento_votante_votato ON pagelle_voti (evento, votante, votato)",
|
||||||
|
],
|
||||||
|
listRule: "@request.auth.id != ''",
|
||||||
|
viewRule: "@request.auth.id != ''",
|
||||||
|
createRule:
|
||||||
|
"@request.auth.role = 'admin' || (@request.auth.id != '' && votante.auth_user_id = @request.auth.id)",
|
||||||
|
updateRule:
|
||||||
|
"@request.auth.role = 'admin' || (@request.auth.id != '' && votante.auth_user_id = @request.auth.id)",
|
||||||
|
deleteRule:
|
||||||
|
"@request.auth.role = 'admin' || (@request.auth.id != '' && votante.auth_user_id = @request.auth.id)",
|
||||||
|
});
|
||||||
|
app.save(pagelleVoti);
|
||||||
|
|
||||||
|
const badgeSocialVoti = new Collection({
|
||||||
|
name: "badge_social_voti",
|
||||||
|
type: "base",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "evento",
|
||||||
|
type: "relation",
|
||||||
|
required: true,
|
||||||
|
collectionId: eventi.id,
|
||||||
|
maxSelect: 1,
|
||||||
|
cascadeDelete: true,
|
||||||
|
},
|
||||||
|
{ name: "categoria", type: "text", required: true },
|
||||||
|
{
|
||||||
|
name: "votante",
|
||||||
|
type: "relation",
|
||||||
|
required: true,
|
||||||
|
collectionId: giocatori.id,
|
||||||
|
maxSelect: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "votato",
|
||||||
|
type: "relation",
|
||||||
|
required: true,
|
||||||
|
collectionId: giocatori.id,
|
||||||
|
maxSelect: 1,
|
||||||
|
},
|
||||||
|
{ name: "votato_nome", type: "text", required: true },
|
||||||
|
{ name: "created", type: "autodate", onCreate: true },
|
||||||
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
"CREATE UNIQUE INDEX idx_badge_social_voti_evento_categoria_votante ON badge_social_voti (evento, categoria, votante)",
|
||||||
|
],
|
||||||
|
listRule: "@request.auth.id != ''",
|
||||||
|
viewRule: "@request.auth.id != ''",
|
||||||
|
createRule:
|
||||||
|
"@request.auth.role = 'admin' || (@request.auth.id != '' && votante.auth_user_id = @request.auth.id)",
|
||||||
|
updateRule:
|
||||||
|
"@request.auth.role = 'admin' || (@request.auth.id != '' && votante.auth_user_id = @request.auth.id)",
|
||||||
|
deleteRule:
|
||||||
|
"@request.auth.role = 'admin' || (@request.auth.id != '' && votante.auth_user_id = @request.auth.id)",
|
||||||
|
});
|
||||||
|
app.save(badgeSocialVoti);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// turni_palloni / scout_sessioni / scout_live / scout_partite — nessun gate in UI,
|
||||||
|
// aperte a qualsiasi autenticato (invariato da Supabase, vedi docs/DATABASE.md).
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
const turniPalloni = new Collection({
|
||||||
|
name: "turni_palloni",
|
||||||
|
type: "base",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "evento",
|
||||||
|
type: "relation",
|
||||||
|
required: true,
|
||||||
|
collectionId: eventi.id,
|
||||||
|
maxSelect: 1,
|
||||||
|
cascadeDelete: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "giocatore",
|
||||||
|
type: "relation",
|
||||||
|
required: true,
|
||||||
|
collectionId: giocatori.id,
|
||||||
|
maxSelect: 1,
|
||||||
|
},
|
||||||
|
{ name: "aggiornato_da", type: "text", required: false },
|
||||||
|
{ name: "created", type: "autodate", onCreate: true },
|
||||||
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
||||||
|
],
|
||||||
|
indexes: ["CREATE UNIQUE INDEX idx_turni_palloni_evento ON turni_palloni (evento)"],
|
||||||
|
listRule: "@request.auth.id != ''",
|
||||||
|
viewRule: "@request.auth.id != ''",
|
||||||
|
createRule: "@request.auth.id != ''",
|
||||||
|
updateRule: "@request.auth.id != ''",
|
||||||
|
deleteRule: "@request.auth.id != ''",
|
||||||
|
});
|
||||||
|
app.save(turniPalloni);
|
||||||
|
|
||||||
|
const scoutSessioni = new Collection({
|
||||||
|
name: "scout_sessioni",
|
||||||
|
type: "base",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "evento",
|
||||||
|
type: "relation",
|
||||||
|
required: true,
|
||||||
|
collectionId: eventi.id,
|
||||||
|
maxSelect: 1,
|
||||||
|
cascadeDelete: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "giocatore",
|
||||||
|
type: "relation",
|
||||||
|
required: true,
|
||||||
|
collectionId: giocatori.id,
|
||||||
|
maxSelect: 1,
|
||||||
|
},
|
||||||
|
{ name: "giocatore_nome", type: "text", required: true },
|
||||||
|
{ name: "created", type: "autodate", onCreate: true },
|
||||||
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
||||||
|
],
|
||||||
|
indexes: ["CREATE UNIQUE INDEX idx_scout_sessioni_evento ON scout_sessioni (evento)"],
|
||||||
|
listRule: "@request.auth.id != ''",
|
||||||
|
viewRule: "@request.auth.id != ''",
|
||||||
|
createRule: "@request.auth.id != ''",
|
||||||
|
updateRule: "@request.auth.id != ''",
|
||||||
|
deleteRule: "@request.auth.id != ''",
|
||||||
|
});
|
||||||
|
app.save(scoutSessioni);
|
||||||
|
|
||||||
|
const scoutLive = new Collection({
|
||||||
|
name: "scout_live",
|
||||||
|
type: "base",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "evento",
|
||||||
|
type: "relation",
|
||||||
|
required: true,
|
||||||
|
collectionId: eventi.id,
|
||||||
|
maxSelect: 1,
|
||||||
|
cascadeDelete: true,
|
||||||
|
},
|
||||||
|
{ name: "stato", type: "json", required: false },
|
||||||
|
{ name: "created", type: "autodate", onCreate: true },
|
||||||
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
||||||
|
],
|
||||||
|
indexes: ["CREATE UNIQUE INDEX idx_scout_live_evento ON scout_live (evento)"],
|
||||||
|
listRule: "@request.auth.id != ''",
|
||||||
|
viewRule: "@request.auth.id != ''",
|
||||||
|
createRule: "@request.auth.id != ''",
|
||||||
|
updateRule: "@request.auth.id != ''",
|
||||||
|
deleteRule: "@request.auth.id != ''",
|
||||||
|
});
|
||||||
|
app.save(scoutLive);
|
||||||
|
|
||||||
|
const scoutPartite = new Collection({
|
||||||
|
name: "scout_partite",
|
||||||
|
type: "base",
|
||||||
|
fields: [
|
||||||
|
// Id libero (come in Supabase, "id text PRIMARY KEY", generato dal client).
|
||||||
|
{ name: "id", type: "text", min: 1, max: 60 },
|
||||||
|
{
|
||||||
|
name: "evento",
|
||||||
|
type: "relation",
|
||||||
|
required: false,
|
||||||
|
collectionId: eventi.id,
|
||||||
|
maxSelect: 1,
|
||||||
|
cascadeDelete: true,
|
||||||
|
},
|
||||||
|
{ name: "data", type: "date", required: true },
|
||||||
|
{ name: "avversario", type: "text", required: true },
|
||||||
|
{ name: "casa", type: "bool", required: false },
|
||||||
|
{ name: "set_nostri", type: "number", required: true, min: 0 },
|
||||||
|
{ name: "set_loro", type: "number", required: true, min: 0 },
|
||||||
|
{ name: "parziali", type: "json", required: false },
|
||||||
|
{ name: "azioni", type: "json", required: false },
|
||||||
|
{ name: "created", type: "autodate", onCreate: true },
|
||||||
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
||||||
|
],
|
||||||
|
listRule: "@request.auth.id != ''",
|
||||||
|
viewRule: "@request.auth.id != ''",
|
||||||
|
createRule: "@request.auth.id != ''",
|
||||||
|
updateRule: "@request.auth.id != ''",
|
||||||
|
deleteRule: "@request.auth.id != ''",
|
||||||
|
});
|
||||||
|
app.save(scoutPartite);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
// push_subscriptions — dispositivi registrati per le notifiche push
|
||||||
|
// ---------------------------------------------------------------------------------------
|
||||||
|
const pushSubscriptions = new Collection({
|
||||||
|
name: "push_subscriptions",
|
||||||
|
type: "base",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "giocatore",
|
||||||
|
type: "relation",
|
||||||
|
required: false,
|
||||||
|
collectionId: giocatori.id,
|
||||||
|
maxSelect: 1,
|
||||||
|
},
|
||||||
|
{ name: "endpoint", type: "text", required: true },
|
||||||
|
{ name: "p256dh", type: "text", required: true },
|
||||||
|
{ name: "auth", type: "text", required: true },
|
||||||
|
{ name: "created", type: "autodate", onCreate: true },
|
||||||
|
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
"CREATE UNIQUE INDEX idx_push_subscriptions_endpoint ON push_subscriptions (endpoint)",
|
||||||
|
],
|
||||||
|
listRule: "@request.auth.id != ''",
|
||||||
|
viewRule: "@request.auth.id != ''",
|
||||||
|
createRule: "@request.auth.id != ''",
|
||||||
|
updateRule: "@request.auth.id != ''",
|
||||||
|
deleteRule: "@request.auth.id != ''",
|
||||||
|
});
|
||||||
|
app.save(pushSubscriptions);
|
||||||
|
},
|
||||||
|
(app) => {
|
||||||
|
const names = [
|
||||||
|
"push_subscriptions",
|
||||||
|
"scout_partite",
|
||||||
|
"scout_live",
|
||||||
|
"scout_sessioni",
|
||||||
|
"turni_palloni",
|
||||||
|
"badge_social_voti",
|
||||||
|
"pagelle_voti",
|
||||||
|
"mvp_voti",
|
||||||
|
"cacche_partita",
|
||||||
|
"risposte_presenze",
|
||||||
|
"eventi_app",
|
||||||
|
"avatar_giocatori",
|
||||||
|
"profili_giocatore",
|
||||||
|
"giocatori_squadra",
|
||||||
|
];
|
||||||
|
for (const name of names) {
|
||||||
|
const collection = app.findCollectionByNameOrId(name);
|
||||||
|
if (collection) app.delete(collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = app.findCollectionByNameOrId("users");
|
||||||
|
users.fields.removeByName("role");
|
||||||
|
app.save(users);
|
||||||
|
},
|
||||||
|
);
|
||||||
Executable
+19
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Ricrea da zero il PocketBase locale (equivalente di "npx supabase db reset").
|
||||||
|
# Ferma il container, azzera pb_data (le migration in pocketbase/pb_migrations/ vengono
|
||||||
|
# riapplicate all'avvio) e riavvia. Il superuser (da POCKETBASE_SUPERUSER_EMAIL/PASSWORD
|
||||||
|
# in .env) e il provider Google OAuth2 (da GOOGLE_OAUTH_CLIENT_ID/SECRET) si ricreano da
|
||||||
|
# soli all'avvio, senza passaggi manuali.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
docker compose -f docker-compose.pocketbase.yml down
|
||||||
|
# Alcuni file (avatar, thumbnail) li scrive il container con un utente diverso da quello
|
||||||
|
# host: un semplice "rm -rf" da host può fallire per permessi, quindi si pulisce da dentro
|
||||||
|
# un container usa-e-getta.
|
||||||
|
docker run --rm -v "$(pwd)/pocketbase/pb_data:/pb_data" alpine sh -c 'rm -rf /pb_data/* /pb_data/.[!.]* 2>/dev/null || true'
|
||||||
|
mkdir -p pocketbase/pb_data
|
||||||
|
touch pocketbase/pb_data/.gitkeep
|
||||||
|
docker compose -f docker-compose.pocketbase.yml up -d
|
||||||
|
|
||||||
|
echo "PocketBase locale ricreato. Dashboard: http://127.0.0.1:8090/_/"
|
||||||
Reference in New Issue
Block a user