Compare commits

46 Commits

Author SHA1 Message Date
juanjo
99de5f06b5 docs: corregir flujo de ramas en CLAUDE.md (pre-dev → dev → master)
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 01:15:04 +02:00
juanjo
fbc5f0f6c4 fix: reemplazar referencias a api_hub_dispatcher por api_config
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
ROOT_URLCONF, WSGI_APPLICATION, DJANGO_SETTINGS_MODULE apuntaban al
módulo renombrado — causaba ModuleNotFoundError al arrancar en Docker.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 01:13:52 +02:00
juanjo
a17f00bad2 fix: env_file .env (relativo a deployments/) en docker-compose
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Jenkins copia el secret a deployments/.env — el path debe ser .env
relativo al docker-compose.yml, no ../.env a la raíz del workspace.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 01:06:28 +02:00
juanjo
41fc2a0aa0 fix: gunicorn + wait-for-db + healthcheck en docker-compose
Some checks failed
DEPLOY_MULTI_BRACH/pipeline/head There was a failure building this commit
- entrypoint.sh: sustituye runserver por gunicorn (workers=2, timeout=120)
- entrypoint.sh: espera a PostgreSQL antes de migrar cuando DB_HOST está definido
- docker-compose.yml: unifica nombre de servicio db, añade healthcheck robusto,
  corrige env_file path a ../.env

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 00:39:21 +02:00
juanjo
001bf13d26 chore: añadir CLAUDE.md con convenciones del ecosistema
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 00:12:18 +02:00
juanjo
47f23a73e1 docs: README completo con despliegue, BD y mantenimiento
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 00:02:21 +02:00
juanjo
04c37f669c revert: restaurar nombre api_config (revertir renombrado erróneo)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 00:02:21 +02:00
juanjo
92a00ec75f refactor: renombrar proyecto principal api_config → api_hub_dispatcher
- Renombrar carpeta app/api_config/ → app/api_hub_dispatcher/
- Actualizar DJANGO_SETTINGS_MODULE en asgi.py, wsgi.py, manage.py
- Actualizar ROOT_URLCONF y WSGI_APPLICATION en settings.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 00:02:21 +02:00
b91f5d09e5 Merge pull request 'dev' (#43) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #43
2026-04-16 17:32:46 +00:00
aee34d797d Merge pull request 'fix engram rtk' (#42) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #42
2026-04-16 16:25:26 +00:00
juanjo
0fc5392bd2 fix engram rtk
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-16 18:24:13 +02:00
1e1348bc1a Merge pull request 'dev' (#41) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #41
2026-04-16 15:34:48 +00:00
f68029edc1 Merge pull request 'fix: serializar dates/Decimal en LogService con DjangoJSONEncoder' (#40) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #40
2026-04-16 15:34:11 +00:00
juanjo
384f47df5e Merge branch 'master' of https://git.v-encore-lab.com/Proyecto-SaaS/django-core-base
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-16 17:29:18 +02:00
juanjo
a8dbb62b09 fix: serializar dates/Decimal en LogService con DjangoJSONEncoder
Some checks are pending
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
DEPLOY_MULTI_BRACH/pipeline/pr-dev Build queued...
- Añadir _json_safe() que usa DjangoJSONEncoder para convertir objetos
  no serializables (datetime.date, Decimal, UUID...) antes de guardar
  en JSONField, evitando que response quede vacío silenciosamente
- Corregir condición status_code: usar 'is not None' para permitir status=0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 17:28:48 +02:00
juanjo
2684e251f7 Merge dev: fix serialización LogService 2026-04-16 17:28:48 +02:00
juanjo
94faedecae Merge pre-dev: fix serialización LogService 2026-04-16 17:28:48 +02:00
3f95e92318 Merge pull request 'dev' (#39) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #39
2026-04-16 15:11:49 +00:00
aed8661331 Merge pull request 'fix: corregir ruta de data/ y hacer LogService resiliente a fallos de BD' (#38) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #38
2026-04-16 15:11:27 +00:00
juanjo
e908125d31 fix: corregir ruta de data/ y hacer LogService resiliente a fallos de BD
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
- Mover data/.gitkeep a app/data/.gitkeep (Django busca app/data/db.sqlite3,
  no data/db.sqlite3 en la raíz del repo, porque BASE_DIR apunta a app/)
- Actualizar .gitignore para ignorar también app/data/*
- LogService.gestionar_log ahora captura excepciones internamente para que
  un fallo de BD no rompa la petición (log omitido con WARNING, no 500)
- Mover import de Log dentro del método para evitar problemas de arranque

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 17:07:23 +02:00
juanjo
03663aacb4 Merge dev: fix ruta data/ y LogService resiliente 2026-04-16 17:07:23 +02:00
juanjo
0c18ffc2f9 Merge pre-dev: fix ruta data/ y LogService resiliente 2026-04-16 17:07:23 +02:00
77b722d739 Merge pull request 'dev' (#37) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #37
2026-04-16 14:57:37 +00:00
3c7d47782b Merge pull request 'feat: añadir app general con LogService centralizado' (#36) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #36
2026-04-16 14:57:20 +00:00
juanjo
156b5ad77d Merge dev: feat app general con LogService 2026-04-16 16:51:00 +02:00
juanjo
5d2a6469aa Merge pre-dev: feat app general con LogService 2026-04-16 16:51:00 +02:00
juanjo
2f6564d9a6 feat: añadir app general con LogService centralizado
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/pr-dev This commit looks good
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
- Crear app/general con estructura estándar del proyecto:
  · utilidades/acciones.py → LogService.gestionar_log() (única fuente de logs)
  · utilidades/utils.py → get_client_ip()
  · utilidades/custom_errors.py → ValidationError, ExternalServiceError, NotFoundError
  · exception.py, request.py, serializers.py, validaciones/
- Registrar 'general' en INSTALLED_APPS y añadir general/ a urls.py
- Refactorizar promociones/views.py para usar LogService en lugar de Log directo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 16:50:53 +02:00
e2ae400889 Merge pull request 'dev' (#35) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #35
2026-04-16 14:37:32 +00:00
e597a05f08 Merge pull request 'refactor: mover BD SQLite a carpeta data/' (#34) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #34
2026-04-16 14:36:39 +00:00
juanjo
4425141cb3 Merge dev: refactor mover BD SQLite a carpeta data/
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-16 16:34:16 +02:00
juanjo
29db0eb0a2 Merge pre-dev: refactor mover BD SQLite a carpeta data/ 2026-04-16 16:34:00 +02:00
juanjo
91fc6900eb refactor: mover BD SQLite a carpeta data/
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
- Mover db.sqlite3 de raíz a data/db.sqlite3
- Actualizar settings.py: fallback SQLite a data/ cuando DB_HOST no está definido
- Actualizar .gitignore: ignorar data/* pero mantener data/.gitkeep
- Actualizar init_db.py: mensaje apunta a data/db.sqlite3
- Añadir data/.gitkeep para anclar la carpeta en git

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 16:33:26 +02:00
0a73b91e12 Merge pull request 'fix: corregir name en common/apps.py' (#33) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-16 14:13:02 +00:00
d444021a00 Merge pull request 'fix: corregir name en common/apps.py' (#32) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-16 14:12:59 +00:00
juanjo
9fba8938ee fix: corregir name en common/apps.py de 'apps.common' a 'common'
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 16:12:40 +02:00
508f3f028d Merge pull request 'refactor: reorganizar estructura app/api_config' (#31) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-16 14:02:19 +00:00
7a151a4768 Merge pull request 'refactor: reorganizar estructura app/api_config' (#30) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-16 14:02:01 +00:00
juanjo
299428741b refactor: reorganizar estructura del proyecto al estándar app/api_config
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
- core/ → app/api_config/
- apps/backend_admin/ → app/backend_admin/
- apps/common/ → app/common/
- apps/promociones/ → app/promociones/
- manage.py → app/manage.py
- Añadir app/requirements.txt
- Actualizar todos los imports y referencias (DJANGO_SETTINGS_MODULE, ROOT_URLCONF, WSGI_APPLICATION, INSTALLED_APPS)
- Actualizar Dockerfile con nuevo WORKDIR

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 16:01:17 +02:00
bc82249a29 Merge pull request 'Merge dev into master' (#29) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-16 13:51:32 +00:00
56e7d77d63 Merge pull request 'Merge pre-dev into dev' (#28) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-16 13:51:17 +00:00
juanjo
27ccce862d Renombrar actions.py a acciones.py en backend_admin y promociones
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
DEPLOY_MULTI_BRACH/pipeline/pr-dev This commit looks good
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 15:42:21 +02:00
f3514d399e Merge pull request 'dev' (#27) from dev into master
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #27
2026-04-14 23:44:35 +00:00
1e72aa3e44 Merge pull request 'Update 0001_initial.py' (#26) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
DEPLOY_MULTI_BRACH/pipeline/pr-master This commit looks good
Reviewed-on: #26
2026-04-14 23:17:11 +00:00
minguezsanzjuanjose
84baf8fbda Update 0001_initial.py
Some checks are pending
DEPLOY_MULTI_BRACH/pipeline/pr-dev Build queued...
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-15 01:16:07 +02:00
b08e74f459 Merge pull request 'fix' (#25) from pre-dev into dev
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
Reviewed-on: #25
2026-04-14 23:02:44 +00:00
minguezsanzjuanjose
5d69f89028 fix
All checks were successful
DEPLOY_MULTI_BRACH/pipeline/head This commit looks good
2026-04-15 01:01:46 +02:00
1079 changed files with 948758 additions and 158 deletions

View File

@@ -0,0 +1,30 @@
{
"permissions": {
"allow": [
"Bash(gh auth:*)",
"Bash(git config:*)",
"Read(//c/Users/juanm/**)",
"Bash(cmdkey /list)",
"Bash(curl -s -X POST https://git.v-encore-lab.com/api/v1/repos/Proyecto-SaaS/django-core-base/pulls -H 'Authorization: token 3b78f0a988a74fcc251d4b5476dd54c7d98c26d2' -H 'Content-Type: application/json' -d '{\"title\":\"Merge pre-dev into dev\",\"head\":\"pre-dev\",\"base\":\"dev\",\"body\":\"Merge de pre-dev a dev\"}')",
"Bash(curl -s -X POST https://git.v-encore-lab.com/api/v1/repos/Proyecto-SaaS/django-core-base/pulls -H 'Authorization: token 3b78f0a988a74fcc251d4b5476dd54c7d98c26d2' -H 'Content-Type: application/json' -d '{\"title\":\"Merge dev into master\",\"head\":\"dev\",\"base\":\"master\",\"body\":\"Merge de dev a master\"}')",
"Bash(curl -s -X POST https://git.v-encore-lab.com/api/v1/repos/Proyecto-SaaS/django-core-base/pulls/28/merge -H 'Authorization: token 3b78f0a988a74fcc251d4b5476dd54c7d98c26d2' -H 'Content-Type: application/json' -d '{\"Do\":\"merge\",\"merge_message_field\":\"Merge pre-dev into dev\"}')",
"Bash(curl -s https://git.v-encore-lab.com/api/v1/repos/Proyecto-SaaS/django-core-base/pulls/28 -H 'Authorization: token 3b78f0a988a74fcc251d4b5476dd54c7d98c26d2')",
"Bash(python -c \"import sys,json; p=json.load\\(sys.stdin\\); print\\('merged:', p['merged'], '| state:', p['state']\\)\")",
"Bash(curl -s -X POST https://git.v-encore-lab.com/api/v1/repos/Proyecto-SaaS/django-core-base/pulls/29/merge -H 'Authorization: token 3b78f0a988a74fcc251d4b5476dd54c7d98c26d2' -H 'Content-Type: application/json' -d '{\"Do\":\"merge\",\"merge_message_field\":\"Merge dev into master\"}')",
"Bash(curl -s https://git.v-encore-lab.com/api/v1/repos/Proyecto-SaaS/django-core-base/pulls/29 -H 'Authorization: token 3b78f0a988a74fcc251d4b5476dd54c7d98c26d2')",
"Bash(mkdir -p app)",
"Bash(mv core:*)",
"Bash(mv apps/backend_admin app/backend_admin)",
"Bash(mv apps/common app/common)",
"Bash(mv apps/promociones app/promociones)",
"WebSearch",
"Bash(npm install:*)",
"Bash(engram init:*)",
"Bash(dir:*)",
"Bash(engram --help:*)",
"Bash(engram remember:*)",
"Bash(engram stats:*)",
"Bash(engram recall:*)"
]
}
}

7
.gitignore vendored
View File

@@ -9,8 +9,13 @@ deployments/docker-compose.yml
# Archivos de datos de la DB local # Archivos de datos de la DB local
postgres_data/ postgres_data/
local_postgres_data/ local_postgres_data/
# Carpeta de datos (BD SQLite y similares), pero se mantiene la carpeta
data/*
!data/.gitkeep
app/data/*
!app/data/.gitkeep
*.pyc *.pyc
apps/backend_admin/migrations/0001_initial.py
# Bloquear todos los .env en cualquier carpeta # Bloquear todos los .env en cualquier carpeta
.env .env
**/core/.env **/core/.env

110
CLAUDE.md Normal file
View File

@@ -0,0 +1,110 @@
# Convenciones del Ecosistema V-Encore Lab
## Ecosistema de microservicios
| Repo | Rol | Puerto |
|------|-----|--------|
| `django-core-base` | Hub orquestador principal | 8000 |
| `api_backoffice` | Consulta y gestión de BD | 8001 |
| `api_comunicaciones` | Emails, notificaciones, webhooks | 8002 |
| `api_documentacion` | Generación y gestión de documentos | 8003 |
| `web_interno` | Panel de gestión (React + Ant Design) | 3000 |
## Stack Django
- Django 5.0 + DRF + SimpleJWT
- Apps bajo `app/`. Prefijo `/api/` en todas las URLs salvo `admin/`.
- Patrón 3 capas: **URL → View → Action**
- SQL con `connection.cursor()` y placeholders (`%s`). Nunca concatenar strings.
## Patrón de vistas — 4 bloques obligatorios
```python
from general.utilidades.acciones import LogService
class MiVista(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
path = '/mi-app/mi-endpoint/'
# Bloque 1 — Inicio Log
log_id = LogService.gestionar_log(self, request, path=path)
try:
# Bloque 2 — Data Cleaning
data = request.data
LogService.gestionar_log(self, request, log_id=log_id,
path=path, body_request=data, status_code=100)
params = {'campo': data.get('campo')}
except Exception as error:
response = {'error': str(error)}
LogService.gestionar_log(self, request, log_id=log_id,
path=path, body_response=response, status_code=400)
return JsonResponse(response, status=400)
try:
# Bloque 3 — Action Call
resultado = mi_accion(params)
# Bloque 4 — Cierre Log
LogService.gestionar_log(self, request, log_id=log_id,
path=path, body_response=resultado, status_code=200)
return JsonResponse(resultado, status=200)
except Exception as error:
response = {'error': str(error)}
LogService.gestionar_log(self, request, log_id=log_id,
path=path, body_response=response, status_code=500)
return JsonResponse(response, status=500)
```
## LogService — reglas
- Siempre llamar como `LogService.gestionar_log(self, request, ...)` — pasar `self` de la vista.
- Primera llamada (sin `log_id`): crea el registro, devuelve el `log_id`.
- Llamadas siguientes: actualizar con `log_id=log_id`.
- `body_request` y `body_response` se serializan con `DjangoJSONEncoder` — soporta `datetime.date`, `Decimal`, etc.
- `status_code` como entero. Usar `is not None` para comprobar (0 es válido).
## Base de datos
- `DB_HOST` definido → PostgreSQL
- `DB_HOST` no definido → SQLite en `app/data/db.sqlite3`
- Migraciones siempre desde `app/`: `cd app && python manage.py migrate`
- SQL INSERT con psycopg2: usar `INSERT ... RETURNING id` + `cursor.fetchone()`. **Nunca `cursor.lastrowid`**.
- Booleans en psycopg2: pasar `True`/`False`, no `1`/`0`.
## Flujo de ramas
```
pre-dev → dev → master
```
- `pre-dev`: desarrollo activo — aquí entran todos los cambios
- `dev`: validación previa a producción
- `master`: producción estable
- Merges siempre con `--no-ff`
- **NUNCA** propagar en sentido inverso (master → dev o dev → pre-dev)
- Secuencia correcta:
```bash
git checkout pre-dev && git merge <mi-cambio> --no-ff
git checkout dev && git merge pre-dev --no-ff && git push origin dev
git checkout master && git merge dev --no-ff && git push origin master
```
## Estructura de app Django
```
app/
├── api_config/ # settings.py, urls.py, wsgi.py
├── general/ # LogService, utils — NO tiene modelos propios
├── backend_admin/ # Modelo Log (audit_logs), admin panel
├── common/ # Modelos y utilidades compartidas entre apps
├── <feature_app>/ # Una app por dominio de negocio
├── data/ # SQLite (gitignored, solo .gitkeep versionado)
└── manage.py
```
## Fixtures
- `loaddata` ignora `auto_now_add=True` — incluir fechas explícitas en el JSON o falla con NOT NULL en PostgreSQL.

329
README.md
View File

@@ -1,73 +1,312 @@
# django-core-base # django-core-base · API Hub Orquestador
// V-Encore Lab: Sistema Automatizado v1.0.4 > V-Encore Lab — Microservicio principal. Actúa como orquestador y punto de entrada central del ecosistema SaaS.
> Puerto por defecto: **8000**
## 🚀 Inicio Rápido (Desarrollo Local) ---
## Tabla de contenidos
1. [Requisitos previos](#requisitos-previos)
2. [Inicio rápido — Desarrollo local](#inicio-rápido--desarrollo-local)
3. [Variables de entorno](#variables-de-entorno)
4. [Base de datos](#base-de-datos)
5. [Docker — Producción](#docker--producción)
6. [Estructura del proyecto](#estructura-del-proyecto)
7. [Endpoints principales](#endpoints-principales)
8. [Patrón LogService](#patrón-logservice)
9. [Flujo de ramas](#flujo-de-ramas)
10. [Mantenimiento](#mantenimiento)
---
## Requisitos previos
| Herramienta | Versión mínima |
|-------------|---------------|
| Python | 3.11 |
| pip | 23+ |
| Docker | 24+ |
| Docker Compose | v2 |
| Git | 2.40+ |
---
## Inicio rápido — Desarrollo local
### 1. Clonar y Configurar
```bash ```bash
# 1. Clonar
git clone https://git.v-encore-lab.com/Proyecto-SaaS/django-core-base.git git clone https://git.v-encore-lab.com/Proyecto-SaaS/django-core-base.git
cd django-core-base cd django-core-base
cp .env.example .env
```
### 2. Instalar Dependencias # 2. Entorno virtual
```bash python -m venv .venv
source .venv/bin/activate # Linux/Mac
.venv\Scripts\activate # Windows
# 3. Dependencias
pip install -r deployments/requirements.txt pip install -r deployments/requirements.txt
```
### 3. Migraciones de Base de Datos # 4. Variables de entorno
```bash cp .env.example .env
# Crear y aplicar migraciones para todos los modelos # Editar .env según el entorno (ver sección Variables de entorno)
python manage.py makemigrations
# 5. Migraciones (SQLite en local)
cd app
python manage.py migrate python manage.py migrate
# Opcional: Crear superusuario # 6. Superusuario (primera vez)
python manage.py createsuperuser
# 7. Servidor de desarrollo
python manage.py runserver 0.0.0.0:8000
```
Accesos:
- API: http://localhost:8000/api/
- Admin: http://localhost:8000/admin/
---
## Variables de entorno
Copia `.env.example` a `.env` y ajusta los valores:
| Variable | Descripción | Ejemplo / Default |
|----------------|--------------------------------------------------|----------------------------|
| `SECRET_KEY` | Clave secreta Django | `django-insecure-...` |
| `DEBUG` | Modo debug | `True` (dev) / `False` (prod) |
| `ALLOWED_HOSTS`| Hosts permitidos (separados por coma) | `localhost,127.0.0.1` |
| `DB_HOST` | Host PostgreSQL. **Si no se define → SQLite** | `localhost` / vacío |
| `DB_PORT` | Puerto PostgreSQL | `5432` |
| `DB_NAME` | Nombre de la base de datos | `vencorelab` |
| `DB_USER` | Usuario PostgreSQL | `postgres` |
| `DB_PASSWORD` | Contraseña PostgreSQL | `postgres` |
---
## Base de datos
### Desarrollo local — SQLite
Si `DB_HOST` **no** está definido en `.env`, Django usa automáticamente SQLite:
```
app/data/db.sqlite3
```
```bash
cd app
python manage.py migrate # crea app/data/db.sqlite3
python manage.py createsuperuser python manage.py createsuperuser
``` ```
### 4. Correr el Servidor La carpeta `app/data/` está en `.gitignore` (solo se versiona `.gitkeep`).
```bash
python manage.py runserver ### Producción — PostgreSQL
Define `DB_HOST` en `.env` para activar el driver PostgreSQL:
```env
DB_HOST=localhost
DB_PORT=5432
DB_NAME=vencorelab
DB_USER=postgres
DB_PASSWORD=supersecret
``` ```
Abrir http://localhost:8000
## 🐳 Docker (Producción/Desarrollo)
```bash ```bash
docker-compose up --build cd app
python manage.py migrate
``` ```
Acceder a: ### Comandos útiles de migración
- App: http://localhost:8000
- Admin: http://localhost:8000/admin/
- DB: localhost:5432 (Postgres)
## 📋 Comandos Django Comunes
```bash ```bash
# Verificar configuración cd app
python manage.py check
# Recopilar static files # Ver migraciones pendientes
python manage.py collectstatic --noinput python manage.py showmigrations
# Test # Crear migraciones de una app
python manage.py test python manage.py makemigrations <app_name>
# Aplicar todas las migraciones
python manage.py migrate
# Revertir migraciones de una app al estado inicial
python manage.py migrate <app_name> zero
``` ```
## 🔧 Estructura del Proyecto ---
``` ## Docker — Producción
├── apps/ # Aplicaciones Django
│ ├── backend_admin/ ```bash
│ ├── common/ # Construir e iniciar
│ └── promociones/ docker-compose up --build -d
├── core/ # Configuración principal
├── deployments/ # Docker, requirements prod # Ver logs
└── manage.py docker-compose logs -f
# Parar
docker-compose down
# Parar y eliminar volúmenes (¡cuidado en producción!)
docker-compose down -v
``` ```
## .env Variables El contenedor expone el puerto **8000**.
Ver `.env.example` para configuración.
Para producción con PostgreSQL, asegúrate de que `.env` tenga `DB_HOST` apuntando al host correcto (puede ser el nombre del servicio en la red Docker).
---
## Estructura del proyecto
```
django-core-base/
├── app/
│ ├── api_config/ # Configuración Django (settings, urls, wsgi)
│ ├── general/ # App transversal
│ │ └── utilidades/
│ │ ├── acciones.py # LogService — auditoría centralizada
│ │ └── utils.py # Utilidades HTTP (get_client_ip, etc.)
│ ├── backend_admin/ # App admin: modelo Log, endpoints de gestión
│ ├── common/ # Modelos y utilidades compartidas
│ ├── promociones/ # App de ejemplo
│ ├── automatizados/ # Endpoints para Jenkins/automatizaciones
│ ├── data/ # Directorio de la BD SQLite (gitignored)
│ │ └── .gitkeep
│ └── manage.py
├── deployments/
│ ├── requirements.txt
│ └── Dockerfile
├── docker-compose.yml
├── .env.example
└── README.md
```
---
## Endpoints principales
| Método | Ruta | Descripción | Auth |
|--------|-------------------------------|-----------------------------------|--------------|
| POST | `/api/token/` | Obtener JWT (login) | No |
| POST | `/api/token/refresh/` | Renovar access token | No |
| GET | `/admin/` | Panel de administración Django | Session |
| POST | `/api/promociones/obtener/` | Consultar promociones | JWT Bearer |
| GET | `/api/general/health/` | Health check | No |
Autenticación: `Authorization: Bearer <access_token>`
---
## Patrón LogService
Todas las vistas deben usar `LogService` para auditoría:
```python
from general.utilidades.acciones import LogService
class MiVista(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
path = '/mi-app/mi-endpoint/'
# Bloque 1 — Inicio log
log_id = LogService.gestionar_log(self, request, path=path)
try:
# Bloque 2 — Limpieza de datos
data = request.data
LogService.gestionar_log(self, request, log_id=log_id,
path=path, body_request=data, status_code=100)
params = {'campo': data.get('campo')}
except Exception as error:
response = {'error': str(error)}
LogService.gestionar_log(self, request, log_id=log_id,
path=path, body_response=response, status_code=400)
return JsonResponse(response, status=400)
try:
# Bloque 3 — Acción
resultado = mi_accion(params)
LogService.gestionar_log(self, request, log_id=log_id,
path=path, body_response=resultado, status_code=200)
return JsonResponse(resultado, status=200)
except Exception as error:
response = {'error': str(error)}
LogService.gestionar_log(self, request, log_id=log_id,
path=path, body_response=response, status_code=500)
return JsonResponse(response, status=500)
```
`LogService` registra automáticamente en la tabla `audit_logs` el usuario, IP, path, request/response y status code.
---
## Flujo de ramas
```
pre-dev → dev → master
```
- `pre-dev`: desarrollo activo, integración de features
- `dev`: validación previa a producción
- `master`: rama de producción estable
```bash
# Crear feature
git checkout pre-dev
git checkout -b feature/mi-feature
# Mergear a pre-dev
git checkout pre-dev
git merge feature/mi-feature --no-ff
# Promover a dev
git checkout dev
git merge pre-dev --no-ff
# Promover a master
git checkout master
git merge dev --no-ff
```
---
## Mantenimiento
```bash
# Limpiar sesiones expiradas
cd app && python manage.py clearsessions
# Ver estado de la BD
cd app && python manage.py dbshell
# Backup SQLite (desarrollo)
cp app/data/db.sqlite3 app/data/db.sqlite3.bak
# Backup PostgreSQL (producción)
pg_dump -U postgres vencorelab > backup_$(date +%Y%m%d).sql
# Actualizar dependencias
pip list --outdated
pip install -U <paquete>
pip freeze > deployments/requirements.txt
# Reiniciar contenedor Docker
docker-compose restart web
# Ver logs del contenedor
docker-compose logs -f web --tail=100
```
---
> **V-Encore Lab** — Sistema Automatizado v1.0
> Repositorio: `Proyecto-SaaS/django-core-base`

View File

@@ -1,4 +1,4 @@
import os import os
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api_config.settings')
application = get_asgi_application() application = get_asgi_application()

View File

@@ -56,9 +56,11 @@ INSTALLED_APPS = [
'corsheaders', 'corsheaders',
# Tus Apps (Asegúrate de que el path sea correcto) # Tus Apps (Asegúrate de que el path sea correcto)
'apps.promociones', 'general',
'apps.backend_admin', 'promociones',
'apps.common', 'automatizados',
'backend_admin',
'common',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@@ -72,7 +74,7 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
] ]
ROOT_URLCONF = 'core.urls' ROOT_URLCONF = 'api_config.urls'
TEMPLATES = [ TEMPLATES = [
{ {
@@ -90,20 +92,29 @@ TEMPLATES = [
}, },
] ]
WSGI_APPLICATION = 'core.wsgi.application' WSGI_APPLICATION = 'api_config.wsgi.application'
# 3. DATABASE # 3. DATABASE
# Extraemos con fallback por si el .env falla # En producción (cuando DB_HOST está definido) usa PostgreSQL.
DATABASES = { # En local/desarrollo sin configuración, cae a SQLite en data/
if os.getenv('DB_HOST'):
DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.postgresql', 'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv('DB_NAME', 'postgres'), 'NAME': os.getenv('DB_NAME', 'postgres'),
'USER': os.getenv('DB_USER', 'postgres'), 'USER': os.getenv('DB_USER', 'postgres'),
'PASSWORD': os.getenv('DB_PASSWORD', ''), 'PASSWORD': os.getenv('DB_PASSWORD', ''),
'HOST': os.getenv('DB_HOST', 'localhost'), 'HOST': os.getenv('DB_HOST'),
'PORT': os.getenv('DB_PORT', '5432'), 'PORT': os.getenv('DB_PORT', '5432'),
} }
} }
else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'data' / 'db.sqlite3',
}
}
# Internationalization # Internationalization
LANGUAGE_CODE = 'es-es' LANGUAGE_CODE = 'es-es'

10
app/api_config/urls.py Normal file
View File

@@ -0,0 +1,10 @@
from django.urls import path, include
from backend_admin import views as admin_views
urlpatterns = [
path('admin/', include('backend_admin.urls')),
path('api/general/', include('general.urls')),
path('api/promociones/', include('promociones.urls')),
path('api/automatizados/', include('automatizados.urls')),
path('api/token/', admin_views.api_token, name='token_obtain_pair'),
]

View File

@@ -2,6 +2,6 @@ import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
# Este es el enlace con tus configuraciones # Este es el enlace con tus configuraciones
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api_config.settings')
application = get_wsgi_application() application = get_wsgi_application()

View File

@@ -0,0 +1,220 @@
import json
import urllib.request
import urllib.error
from datetime import datetime
from django.db import connection
from django.utils import timezone
from common.utils import clean_sql_string, clean_sql_int
# =========================================================
# Helper HTTP (stdlib) — evita dependencia extra en requirements
# =========================================================
def _http_get_json(url, timeout=8):
"""GET a un servicio externo que responde JSON. Usa stdlib para no añadir deps."""
req = urllib.request.Request(url, headers={'User-Agent': 'django-core-base/1.0'})
with urllib.request.urlopen(req, timeout=timeout) as response:
raw = response.read().decode('utf-8')
try:
return json.loads(raw)
except json.JSONDecodeError:
return {'raw': raw}
def _http_get_text(url, timeout=8):
"""GET a un servicio externo que responde texto plano."""
req = urllib.request.Request(url, headers={'User-Agent': 'django-core-base/1.0'})
with urllib.request.urlopen(req, timeout=timeout) as response:
return response.read().decode('utf-8').strip()
# =========================================================
# ACCIÓN 1 — Base de datos (INSERT histórico de ejecución)
# set_parameterized
# =========================================================
def setEjecucion(params):
"""
Inserta una fila en automatizacion_ejecuciones registrando una ejecución.
params esperados: nombre, descripcion, estado, origen, resultado, error
"""
query = """
INSERT INTO automatizacion_ejecuciones
(nombre, descripcion, estado, origen, resultado, error, fecha_inicio, fecha_fin, activo)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id
"""
ahora = timezone.now()
resultado_json = json.dumps(params.get('resultado')) if params.get('resultado') is not None else None
parameter_dict = [
clean_sql_string(params.get('nombre')),
clean_sql_string(params.get('descripcion')),
clean_sql_string(params.get('estado') or 'ok'),
clean_sql_string(params.get('origen') or 'manual'),
resultado_json,
clean_sql_string(params.get('error')) if params.get('error') else None,
ahora,
ahora,
bool(params.get('activo', True)),
]
with connection.cursor() as cursor:
cursor.execute(query, parameter_dict)
row = cursor.fetchone()
nuevo_id = row[0] if row else None
return {'id': nuevo_id, 'fecha': ahora.isoformat()}
# =========================================================
# ACCIÓN 2 — Servicio externo: Cat Fact API
# https://catfact.ninja/fact (gratis, sin auth)
# =========================================================
def getCatFact():
url = 'https://catfact.ninja/fact'
data = _http_get_json(url)
return {
'servicio': 'catfact.ninja',
'url': url,
'fact': data.get('fact'),
'length': data.get('length'),
}
# =========================================================
# ACCIÓN 3 — Servicio externo: GitHub Zen
# https://api.github.com/zen (gratis, sin auth)
# =========================================================
def getGithubZen():
url = 'https://api.github.com/zen'
texto = _http_get_text(url)
return {
'servicio': 'api.github.com/zen',
'url': url,
'zen': texto,
}
# =========================================================
# Orquestador — ejecuta las 3 acciones en secuencia
# =========================================================
def ejecutarAutomatizaciones(params):
"""
Ejecuta las 3 acciones automatizadas:
1) Llamada a servicio externo: catfact.ninja
2) Llamada a servicio externo: api.github.com/zen
3) Persistencia en BD: inserta la ejecución en automatizacion_ejecuciones
"""
resultados = {'acciones': []}
errores = []
# Acción 2 — catfact
try:
resultados['acciones'].append({'step': 1, 'ok': True, 'data': getCatFact()})
except Exception as err:
errores.append(f'catfact: {err}')
resultados['acciones'].append({'step': 1, 'ok': False, 'error': str(err)})
# Acción 3 — github zen
try:
resultados['acciones'].append({'step': 2, 'ok': True, 'data': getGithubZen()})
except Exception as err:
errores.append(f'github_zen: {err}')
resultados['acciones'].append({'step': 2, 'ok': False, 'error': str(err)})
# Acción 1 — persistir en BD
estado_final = 'ok' if not errores else 'error'
nombre = clean_sql_string(params.get('nombre')) or f'Ejecucion {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}'
origen = clean_sql_string(params.get('origen')) or 'manual'
registro = setEjecucion({
'nombre': nombre,
'descripcion': params.get('descripcion') or 'Ejecución orquestada por /api/automatizados/ejecutar/',
'estado': estado_final,
'origen': origen,
'resultado': resultados,
'error': '; '.join(errores) if errores else None,
'activo': True,
})
resultados['acciones'].append({'step': 3, 'ok': True, 'data': registro})
return {
'ejecucion_id': registro.get('id'),
'estado': estado_final,
'total_acciones': len(resultados['acciones']),
'resultados': resultados,
'errores': errores,
}
# =========================================================
# Lectura — historial (get_parameterized)
# =========================================================
def getHistorial(params):
"""
Devuelve el histórico de ejecuciones. Permite filtrar por estado y limit.
"""
query = """
SELECT id, nombre, descripcion, estado, origen, resultado, error,
fecha_inicio, fecha_fin, activo
FROM automatizacion_ejecuciones
WHERE (%s = '' OR estado = %s)
ORDER BY fecha_inicio DESC
LIMIT %s
"""
estado = clean_sql_string(params.get('estado')) if params.get('estado') else ''
limite = clean_sql_int(params.get('limit')) or 20
parameter_dict = [estado, estado, limite]
with connection.cursor() as cursor:
cursor.execute(query, parameter_dict)
columns = [col[0] for col in cursor.description]
rows = [dict(zip(columns, row)) for row in cursor.fetchall()]
# Normalizamos el campo resultado (JSON serializado en SQLite)
for row in rows:
if isinstance(row.get('resultado'), str):
try:
row['resultado'] = json.loads(row['resultado'])
except (ValueError, TypeError):
pass
return {'total': len(rows), 'data': rows}
# =========================================================
# Estado del módulo
# =========================================================
def getEstado():
"""
Devuelve el estado general del módulo de automatizaciones:
cuántas ejecuciones hay, última ejecución, endpoints disponibles.
"""
query = """
SELECT COUNT(*) AS total,
SUM(CASE WHEN estado = 'ok' THEN 1 ELSE 0 END) AS ok,
SUM(CASE WHEN estado = 'error' THEN 1 ELSE 0 END) AS errores,
MAX(fecha_inicio) AS ultima_ejecucion
FROM automatizacion_ejecuciones
"""
with connection.cursor() as cursor:
cursor.execute(query)
columns = [col[0] for col in cursor.description]
row = cursor.fetchone()
resumen = dict(zip(columns, row)) if row else {}
return {
'status': 'ok',
'modulo': 'automatizados',
'endpoints': [
'POST /api/automatizados/ejecutar/',
'POST /api/automatizados/historial/',
'POST /api/automatizados/estado/',
],
'resumen': resumen,
}

View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class AutomatizadosConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'automatizados'

View File

@@ -0,0 +1,47 @@
[
{
"model": "automatizados.automatizacionejecucion",
"pk": 1,
"fields": {
"nombre": "Ejecución inicial de prueba",
"descripcion": "Seed de ejemplo para poblar la tabla de ejecuciones automatizadas",
"estado": "ok",
"origen": "seed",
"resultado": {"acciones": 0, "detalle": "fixture de arranque"},
"error": null,
"fecha_inicio": "2026-04-16T10:00:00Z",
"fecha_fin": "2026-04-16T10:00:05Z",
"activo": true
}
},
{
"model": "automatizados.automatizacionejecucion",
"pk": 2,
"fields": {
"nombre": "Chequeo nocturno",
"descripcion": "Ejemplo de ejecución recurrente programada",
"estado": "ok",
"origen": "jenkins",
"resultado": {"acciones": 3, "detalle": "todas las llamadas OK"},
"error": null,
"fecha_inicio": "2026-04-16T03:00:00Z",
"fecha_fin": "2026-04-16T03:00:12Z",
"activo": true
}
},
{
"model": "automatizados.automatizacionejecucion",
"pk": 3,
"fields": {
"nombre": "Ejecución con error simulado",
"descripcion": "Registro de referencia para estado=error",
"estado": "error",
"origen": "manual",
"resultado": null,
"error": "timeout al llamar servicio externo",
"fecha_inicio": "2026-04-15T18:30:00Z",
"fecha_fin": "2026-04-15T18:30:08Z",
"activo": false
}
}
]

View File

View File

@@ -0,0 +1,25 @@
from django.db import models
class AutomatizacionEjecucion(models.Model):
"""
Registro histórico de ejecuciones automatizadas.
Cada vez que el endpoint `ejecutar/` corre, se guarda una fila con el
resultado consolidado de las acciones ejecutadas.
"""
nombre = models.CharField(max_length=255)
descripcion = models.TextField(null=True, blank=True)
estado = models.CharField(max_length=50, default='pendiente') # pendiente | ok | error
origen = models.CharField(max_length=100, default='manual') # manual | jenkins | cron
resultado = models.JSONField(null=True, blank=True)
error = models.TextField(null=True, blank=True)
fecha_inicio = models.DateTimeField(auto_now_add=True)
fecha_fin = models.DateTimeField(null=True, blank=True)
activo = models.BooleanField(default=True)
class Meta:
db_table = 'automatizacion_ejecuciones'
ordering = ['-fecha_inicio']
def __str__(self):
return f'{self.nombre} [{self.estado}]'

View File

@@ -0,0 +1,8 @@
from django.urls import path
from .views import AutomatizadosEjecutar, AutomatizadosHistorial, AutomatizadosEstado
urlpatterns = [
path('ejecutar/', AutomatizadosEjecutar.as_view(), name='automatizados_ejecutar'),
path('historial/', AutomatizadosHistorial.as_view(), name='automatizados_historial'),
path('estado/', AutomatizadosEstado.as_view(), name='automatizados_estado'),
]

132
app/automatizados/views.py Normal file
View File

@@ -0,0 +1,132 @@
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework_simplejwt.authentication import JWTAuthentication
from django.http import JsonResponse
from general.utilidades.acciones import LogService
from .acciones import ejecutarAutomatizaciones, getHistorial, getEstado
class AutomatizadosEjecutar(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
path = '/automatizados/ejecutar/'
# --- BLOQUE 1: Inicio Log ---
log_id = LogService.gestionar_log(self, request, path=path)
try:
# --- BLOQUE 2: Data Cleaning ---
data = request.data
status = 100
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_request=data, status_code=status)
params = {
'nombre': data.get('nombre'),
'descripcion': data.get('descripcion'),
'origen': data.get('origen') or 'manual',
}
except Exception as error:
response = {'body': {'data': [], 'error': str(error)}, 'mensaje': str(error)}
status = 400
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, status=status, safe=False)
try:
# --- BLOQUE 3: Action Call ---
resultado = ejecutarAutomatizaciones(params)
response = resultado
status = 200
# --- BLOQUE 4: Cierre Log ---
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, safe=False, status=status)
except Exception as error:
response = {'body': {'data': [], 'error': str(error)}, 'error': str(error)}
status = 500
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, status=status, safe=False)
class AutomatizadosHistorial(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
path = '/automatizados/historial/'
# --- BLOQUE 1: Inicio Log ---
log_id = LogService.gestionar_log(self, request, path=path)
try:
# --- BLOQUE 2: Data Cleaning ---
data = request.data
status = 100
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_request=data, status_code=status)
params = {
'estado': data.get('estado'),
'limit': data.get('limit') or 20,
}
except Exception as error:
response = {'body': {'data': [], 'error': str(error)}, 'mensaje': str(error)}
status = 400
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, status=status, safe=False)
try:
# --- BLOQUE 3: Action Call ---
resultado = getHistorial(params)
response = resultado
status = 200
# --- BLOQUE 4: Cierre Log ---
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, safe=False, status=status)
except Exception as error:
response = {'body': {'data': [], 'error': str(error)}, 'error': str(error)}
status = 500
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, status=status, safe=False)
class AutomatizadosEstado(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
path = '/automatizados/estado/'
# --- BLOQUE 1: Inicio Log ---
log_id = LogService.gestionar_log(self, request, path=path)
try:
# --- BLOQUE 2: Data Cleaning ---
data = request.data
status = 100
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_request=data, status_code=status)
except Exception as error:
response = {'body': {'data': [], 'error': str(error)}, 'mensaje': str(error)}
status = 400
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, status=status, safe=False)
try:
# --- BLOQUE 3: Action Call ---
resultado = getEstado()
response = resultado
status = 200
# --- BLOQUE 4: Cierre Log ---
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, safe=False, status=status)
except Exception as error:
response = {'body': {'data': [], 'error': str(error)}, 'error': str(error)}
status = 500
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, status=status, safe=False)

View File

@@ -0,0 +1,35 @@
# Generated by Django 5.0.3 on 2026-04-14 23:15
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Log',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('user_id', models.IntegerField(default=0)),
('user', models.CharField(default='anonimo', max_length=255)),
('app_id', models.IntegerField(default=0)),
('remote_address', models.GenericIPAddressField(blank=True, null=True)),
('request', models.JSONField(blank=True, null=True)),
('response', models.JSONField(blank=True, null=True)),
('status_code', models.CharField(default='0', max_length=10)),
('path', models.CharField(max_length=255)),
('method', models.CharField(max_length=10)),
('createdAt', models.DateTimeField(default=django.utils.timezone.now)),
('updatedAt', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'audit_logs',
},
),
]

View File

@@ -17,9 +17,10 @@ class Log(models.Model):
createdAt = models.DateTimeField(default=timezone.now) createdAt = models.DateTimeField(default=timezone.now)
updatedAt = models.DateTimeField(auto_now=True) updatedAt = models.DateTimeField(auto_now=True)
class Meta: class Meta:
db_table = 'audit_logs' db_table = 'audit_logs'
managed = False # Al estar la tabla ya creada, Django no intentará modificarla # managed = True <-- Asegúrate de que no esté en False
def __str__(self): def __str__(self):
return f"{self.method} {self.path} ({self.status_code})" return f"{self.method} {self.path} ({self.status_code})"

View File

@@ -1,5 +1,5 @@
from django.http import JsonResponse from django.http import JsonResponse
from .actions import Admin from .acciones import Admin
import logging import logging
import json import json
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt

View File

@@ -3,4 +3,4 @@ from django.apps import AppConfig
class CommonConfig(AppConfig): class CommonConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.common' # <--- ESTO ES LO QUE DEBE PONER name = 'common'

0
app/data/.gitkeep Normal file
View File

0
app/general/__init__.py Normal file
View File

1
app/general/admin.py Normal file
View File

@@ -0,0 +1 @@
from django.contrib import admin

6
app/general/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class GeneralConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'general'

21
app/general/exception.py Normal file
View File

@@ -0,0 +1,21 @@
from rest_framework.views import exception_handler
from rest_framework.response import Response
def custom_exception_handler(exc, context):
"""
Handler global de excepciones para DRF.
Añadir en settings.py:
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'general.exception.custom_exception_handler',
}
"""
response = exception_handler(exc, context)
if response is not None:
response.data = {
'body': {'data': [], 'error': response.data},
'mensaje': str(exc),
}
return response

View File

5
app/general/models.py Normal file
View File

@@ -0,0 +1,5 @@
from django.db import models
# La app general no define modelos propios.
# El modelo Log vive en backend_admin.models y se accede desde
# general.utilidades.acciones.LogService.

24
app/general/request.py Normal file
View File

@@ -0,0 +1,24 @@
import json
from django.http import JsonResponse
def parse_body(request):
"""
Parsea el body de la request como JSON.
Lanza ValueError si el body está vacío o no es JSON válido.
"""
raw = request.body
if not raw:
raise ValueError('El body de la petición está vacío.')
return json.loads(raw)
def build_error_response(message, status=400, data=None):
"""Construye una respuesta de error en el formato estándar del proyecto."""
body = {'data': data if data is not None else [], 'error': message}
return JsonResponse({'body': body, 'mensaje': message}, status=status, safe=False)
def build_success_response(data, status=200):
"""Construye una respuesta de éxito en el formato estándar del proyecto."""
return JsonResponse(data, status=status, safe=False)

View File

@@ -0,0 +1,6 @@
from rest_framework import serializers
class EmptySerializer(serializers.Serializer):
"""Serializer base vacío para endpoints sin body estructurado."""
pass

7
app/general/tests.py Normal file
View File

@@ -0,0 +1,7 @@
from django.test import TestCase
class LogServiceTest(TestCase):
def test_placeholder(self):
"""Placeholder: añadir tests de LogService aquí."""
pass

6
app/general/urls.py Normal file
View File

@@ -0,0 +1,6 @@
from django.urls import path
from .views import status_view
urlpatterns = [
path('status/', status_view, name='general_status'),
]

View File

View File

@@ -0,0 +1,114 @@
import json
import logging
from django.utils import timezone
from django.core.serializers.json import DjangoJSONEncoder
from . import utils
logger = logging.getLogger(__name__)
def _json_safe(value):
"""
Convierte cualquier valor a un tipo seguro para JSONField.
Usa DjangoJSONEncoder para manejar date, datetime, Decimal, UUID, etc.
"""
if value is None or value == '':
return value
try:
return json.loads(json.dumps(value, cls=DjangoJSONEncoder))
except Exception:
return str(value)
class LogService:
"""
Servicio centralizado de gestión de logs.
Única fuente de inserción en la tabla audit_logs.
Uso en vistas:
log_id = LogService.gestionar_log(self, request, path='/mi/path/')
LogService.gestionar_log(self, request, log_id=log_id, body_request=data, status_code=200)
Si la BD no está disponible, el log falla silenciosamente para no
interrumpir la petición del usuario.
"""
def gestionar_log(
self,
request,
log_id=None,
path=None,
user=None,
body_request=None,
body_response=None,
status_code=None,
):
try:
return LogService._ejecutar_log(
self, request, log_id, path, user, body_request, body_response, status_code
)
except Exception as e:
logger.warning('LogService.gestionar_log falló (log omitido): %s', str(e))
return log_id # Devuelve el log_id existente o None si era creación
@staticmethod
def _ejecutar_log(caller, request, log_id, path, user, body_request, body_response, status_code):
# Importación diferida para evitar problemas de arranque si la BD no está lista
from backend_admin.models import Log
# Determinar la app llamante a partir del módulo de la vista
modulo = caller.__class__.__module__
app_nombre = modulo.split('.')[0]
tag_header = request.headers.get('tag')
if tag_header:
usuario_final = tag_header
elif app_nombre:
apps_automatizadas = ['automatizados']
if app_nombre.lower() in apps_automatizadas:
usuario_final = 'Jenkins'
else:
usuario_final = app_nombre
else:
usuario_final = user if user else 'orquestador'
if log_id is None:
# --- CREACIÓN: primer registro del ciclo de vida de la petición ---
path_final = path if path else request.path
nuevo_log = Log.objects.create(
user_id=0,
user=usuario_final,
app_id=0,
remote_address=utils.get_client_ip(request),
request='',
response='',
status_code=status_code if status_code else '0',
path=path_final,
method=request.method,
createdAt=timezone.now(),
)
return nuevo_log.pk
else:
# --- ACTUALIZACIÓN: enriquece el log con datos del procesamiento ---
datos_a_actualizar = {
'updatedAt': timezone.now(),
}
if user:
datos_a_actualizar['user'] = user
if path:
datos_a_actualizar['path'] = path
if body_request is not None:
# _json_safe convierte dates, Decimals, etc. a tipos JSON válidos
datos_a_actualizar['request'] = _json_safe(body_request)
if body_response is not None:
datos_a_actualizar['response'] = _json_safe(body_response)
if status_code is not None:
datos_a_actualizar['status_code'] = str(status_code)
Log.objects.filter(pk=log_id).update(**datos_a_actualizar)
return log_id

View File

@@ -0,0 +1,33 @@
class ValidationError(Exception):
"""Error de validación de datos de entrada."""
def __init__(self, message, field=None):
self.message = message
self.field = field
super().__init__(message)
def to_response(self):
detail = {'error': self.message}
if self.field:
detail['field'] = self.field
return {'body': {'data': [], **detail}, 'mensaje': self.message}
class ExternalServiceError(Exception):
"""Error en la comunicación con un servicio externo."""
def __init__(self, message, service=None):
self.message = message
self.service = service
super().__init__(message)
def to_response(self):
return {'body': {'data': [], 'error': self.message}, 'mensaje': self.message}
class NotFoundError(Exception):
"""Recurso no encontrado."""
def __init__(self, message='Recurso no encontrado'):
self.message = message
super().__init__(message)
def to_response(self):
return {'body': {'data': [], 'error': self.message}, 'mensaje': self.message}

View File

@@ -0,0 +1,6 @@
def get_client_ip(request):
"""Obtiene la IP real del cliente, considerando proxies."""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
return x_forwarded_for.split(',')[0].strip()
return request.META.get('REMOTE_ADDR')

View File

5
app/general/views.py Normal file
View File

@@ -0,0 +1,5 @@
from django.http import JsonResponse
def status_view(request):
return JsonResponse({'status': 'ok', 'service': 'general'})

View File

@@ -6,7 +6,7 @@ import sys
def main(): def main():
"""Run administrative tasks.""" """Run administrative tasks."""
# Apuntamos a la configuración dentro de la carpeta 'core' # Apuntamos a la configuración dentro de la carpeta 'core'
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api_config.settings')
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: except ImportError as exc:

View File

@@ -1,5 +1,5 @@
from django.db import connection from django.db import connection
from apps.common.utils import clean_sql_string, clean_sql_int from common.utils import clean_sql_string, clean_sql_int
def get_status_action(): def get_status_action():

View File

@@ -2,4 +2,4 @@ from django.apps import AppConfig
class PromocionesConfig(AppConfig): class PromocionesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.promociones' name = 'promociones'

46
app/promociones/views.py Normal file
View File

@@ -0,0 +1,46 @@
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework_simplejwt.authentication import JWTAuthentication
from django.http import JsonResponse
from general.utilidades.acciones import LogService
from .acciones import getData
class PromocionObtener(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
path = '/promociones/obtener/'
# --- LOG: inicio de la petición ---
log_id = LogService.gestionar_log(self, request, path=path)
try:
# --- BLOQUE 2: limpieza y validación del body ---
data = request.data
status = 100
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_request=data, status_code=status)
params = {'id': data.get('id'), 'activo': data.get('activo')}
except Exception as error:
response = {'body': {'data': [], 'error': str(error)}, 'mensaje': str(error)}
status = 400
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, status=status, safe=False)
try:
# --- BLOQUE 3: llamada a la acción ---
resultado = getData(params)
response = resultado
status = 200
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, safe=False, status=status)
except Exception as error:
response = {'body': {'data': [], 'error': str(error)}, 'error': str(error)}
status = 500
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
return JsonResponse(response, status=status, safe=False)

7
app/requirements.txt Normal file
View File

@@ -0,0 +1,7 @@
Django==5.0.3
psycopg2-binary==2.9.9
gunicorn==21.2.0
python-dotenv==1.0.1
djangorestframework
django-cors-headers
djangorestframework-simplejwt

View File

@@ -1,46 +0,0 @@
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework_simplejwt.authentication import JWTAuthentication
from django.http import JsonResponse
from apps.backend_admin.models import Log
from .actions import getData
from django.utils import timezone # Esta es la forma correcta
class PromocionObtener(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
# --- BLOQUE 1: LOG INITIATION ---
log_entry = Log.objects.create(
user=request.user.username,
path='promociones/obtener/',
method='POST',
status_code='0'
)
print('llega a despues de log entry')
try:
# --- BLOQUE 2: DATA CLEANING ---
data = request.data
log_entry.request = data
log_entry.save()
params = {'id': data.get('id'), 'activo': data.get('activo')}
# --- BLOQUE 3: ACTION CALL ---
# Aquí ya recibimos las fechas como strings gracias al paso 1
resultado = getData(params)
# --- BLOQUE 4: LOG CLOSURE & RESPONSE ---
log_entry.response = {"count": len(resultado)} # No guardes todo el JSON si es muy grande
log_entry.status_code = '200'
log_entry.save()
return JsonResponse(resultado, safe=False, status=200)
except Exception as e:
log_entry.status_code = '500'
log_entry.response = {'error': str(e)}
log_entry.save()
return JsonResponse({'error': str(e)}, status=500)

View File

@@ -1,10 +0,0 @@
from django.urls import path, include
from django.urls import path, include
from apps.backend_admin import views as admin_views
urlpatterns = [
path('admin/', include('apps.backend_admin.urls')),
# AÑADE ESTA LÍNEA AQUÍ PARA QUE SEA UNA RUTA PRINCIPAL:
path('promociones/', include('apps.promociones.urls')),
path('api/token/', admin_views.api_token, name='token_obtain_pair'),
]

0
data/.gitkeep Normal file
View File

Binary file not shown.

View File

@@ -22,14 +22,14 @@ COPY deployments/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# 6. COPIAR EL CÓDIGO # 6. COPIAR EL CÓDIGO
# Copiamos todo el contenido de la raíz (.) al directorio de trabajo (/app)
# Esto incluirá apps/, core/, manage.py, etc.
COPY . . COPY . .
# Cambiamos al directorio de la app donde está manage.py
WORKDIR /app/app
# 7. EXPOSICIÓN Y SCRIPT DE ENTRADA # 7. EXPOSICIÓN Y SCRIPT DE ENTRADA
EXPOSE 8000 EXPOSE 8000
# El entrypoint también está en deployments/
COPY deployments/entrypoint.sh /entrypoint.sh COPY deployments/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh

View File

@@ -1,44 +1,40 @@
version: '3.8' version: '3.8'
services: services:
gitea-db: db:
image: postgres:15 image: postgres:15-alpine
# Usará el nombre de tu .env (django_db_local) container_name: ${DB_CONTAINER_NAME:-django_core_db}
container_name: ${DB_CONTAINER_NAME:-django_db_dev} restart: unless-stopped
restart: always
environment: environment:
POSTGRES_DB: ${DB_NAME:-gitea} POSTGRES_DB: ${DB_NAME:-django_core_db}
POSTGRES_USER: ${DB_USER:-gitea} POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-gitea} POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
# --- ESTO ES LO QUE FALTA ---
ports: ports:
- "${DATABASE_EXPOSE_PORT:-5432}:5432" - "${DATABASE_EXPOSE_PORT:-5432}:5432"
# ----------------------------
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-gitea} -d ${DB_NAME:-gitea}"] test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-django_core_db}"]
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 5 retries: 10
start_period: 10s
web: web:
build: build:
context: .. context: ..
dockerfile: deployments/Dockerfile dockerfile: deployments/Dockerfile
container_name: ${APP_CONTAINER_NAME:-django_app_dev} container_name: ${APP_CONTAINER_NAME:-django_core_app}
restart: always restart: unless-stopped
env_file: env_file:
- .env - .env
environment: environment:
- DEBUG=${DEBUG_MODE:-1} - DB_HOST=db
# IMPORTANTE: Este nombre debe coincidir con el nombre del servicio arriba (gitea-db)
- DB_HOST=gitea-db
- DB_PORT=5432 - DB_PORT=5432
ports: ports:
- "${PORT:-8000}:8000" - "${PORT:-8000}:8000"
depends_on: depends_on:
gitea-db: db:
condition: service_healthy condition: service_healthy
volumes: volumes:

View File

@@ -3,16 +3,39 @@
# Salir inmediatamente si un comando falla # Salir inmediatamente si un comando falla
set -e set -e
# --- Esperar a PostgreSQL si estamos en modo BD remota ---
if [ -n "$DB_HOST" ]; then
echo "--> Esperando a PostgreSQL en $DB_HOST:${DB_PORT:-5432}..."
until python -c "
import sys, psycopg2, os
try:
psycopg2.connect(
host=os.environ['DB_HOST'],
port=os.environ.get('DB_PORT', 5432),
user=os.environ['DB_USER'],
password=os.environ['DB_PASSWORD'],
dbname=os.environ['DB_NAME']
)
sys.exit(0)
except Exception:
sys.exit(1)
" 2>/dev/null; do
echo " PostgreSQL no disponible, reintentando en 2s..."
sleep 2
done
echo "--> PostgreSQL listo."
fi
echo "--> Ejecutando migraciones..." echo "--> Ejecutando migraciones..."
# Esto asegura que si hay cambios en models.py, se generen y apliquen las tablas
python manage.py makemigrations --noinput
python manage.py migrate --noinput python manage.py migrate --noinput
echo "--> Cargando datos de prueba..." echo "--> Cargando semillas (si existen)..."
# Este comando busca archivos JSON en las carpetas 'fixtures' de tus apps python manage.py loaddata semillas 2>/dev/null || echo " Sin semillas, continuando."
# Usamos || true para que si el archivo no existe o ya están cargados, el contenedor no se detenga
python manage.py loaddata semillas || echo "Aviso: No se pudieron cargar las semillas (fichero no encontrado o error de formato)."
echo "--> Arrancando el servidor Django..." echo "--> Arrancando servidor con Gunicorn..."
# Usamos exec para que Django sea el proceso principal (PID 1) y reciba señales de Docker exec gunicorn api_config.wsgi:application \
exec python manage.py runserver 0.0.0.0:8000 --bind 0.0.0.0:8000 \
--workers 2 \
--timeout 120 \
--access-logfile - \
--error-logfile -

12
frontend/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Django Core Base</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

16
frontend/node_modules/.bin/nanoid generated vendored Normal file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../nanoid/bin/nanoid.cjs" "$@"
else
exec node "$basedir/../nanoid/bin/nanoid.cjs" "$@"
fi

17
frontend/node_modules/.bin/nanoid.cmd generated vendored Normal file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\nanoid\bin\nanoid.cjs" %*

28
frontend/node_modules/.bin/nanoid.ps1 generated vendored Normal file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
} else {
& "$basedir/node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
} else {
& "node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
frontend/node_modules/.bin/rolldown generated vendored Normal file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../rolldown/bin/cli.mjs" "$@"
else
exec node "$basedir/../rolldown/bin/cli.mjs" "$@"
fi

17
frontend/node_modules/.bin/rolldown.cmd generated vendored Normal file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\rolldown\bin\cli.mjs" %*

28
frontend/node_modules/.bin/rolldown.ps1 generated vendored Normal file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../rolldown/bin/cli.mjs" $args
} else {
& "$basedir/node$exe" "$basedir/../rolldown/bin/cli.mjs" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../rolldown/bin/cli.mjs" $args
} else {
& "node$exe" "$basedir/../rolldown/bin/cli.mjs" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
frontend/node_modules/.bin/tsc generated vendored Normal file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
else
exec node "$basedir/../typescript/bin/tsc" "$@"
fi

17
frontend/node_modules/.bin/tsc.cmd generated vendored Normal file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\typescript\bin\tsc" %*

28
frontend/node_modules/.bin/tsc.ps1 generated vendored Normal file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../typescript/bin/tsc" $args
} else {
& "$basedir/node$exe" "$basedir/../typescript/bin/tsc" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../typescript/bin/tsc" $args
} else {
& "node$exe" "$basedir/../typescript/bin/tsc" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
frontend/node_modules/.bin/tsserver generated vendored Normal file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
else
exec node "$basedir/../typescript/bin/tsserver" "$@"
fi

17
frontend/node_modules/.bin/tsserver.cmd generated vendored Normal file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\typescript\bin\tsserver" %*

28
frontend/node_modules/.bin/tsserver.ps1 generated vendored Normal file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../typescript/bin/tsserver" $args
} else {
& "$basedir/node$exe" "$basedir/../typescript/bin/tsserver" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../typescript/bin/tsserver" $args
} else {
& "node$exe" "$basedir/../typescript/bin/tsserver" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
frontend/node_modules/.bin/vite generated vendored Normal file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../vite/bin/vite.js" "$@"
else
exec node "$basedir/../vite/bin/vite.js" "$@"
fi

17
frontend/node_modules/.bin/vite.cmd generated vendored Normal file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\vite\bin\vite.js" %*

28
frontend/node_modules/.bin/vite.ps1 generated vendored Normal file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../vite/bin/vite.js" $args
} else {
& "$basedir/node$exe" "$basedir/../vite/bin/vite.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../vite/bin/vite.js" $args
} else {
& "node$exe" "$basedir/../vite/bin/vite.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

536
frontend/node_modules/.package-lock.json generated vendored Normal file
View File

@@ -0,0 +1,536 @@
{
"name": "frontend",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/@oxc-project/types": {
"version": "0.124.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz",
"integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Boshen"
}
},
"node_modules/@reduxjs/toolkit": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
"integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@standard-schema/utils": "^0.3.0",
"immer": "^11.0.0",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"reselect": "^5.1.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-redux": {
"optional": true
}
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz",
"integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.7",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz",
"integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==",
"dev": true,
"license": "MIT"
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"license": "MIT"
},
"node_modules/@standard-schema/utils": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT"
},
"node_modules/@types/react": {
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
}
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT"
},
"node_modules/@vitejs/plugin-react": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
"integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rolldown/pluginutils": "1.0.0-rc.7"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"peerDependencies": {
"@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
"babel-plugin-react-compiler": "^1.0.0",
"vite": "^8.0.0"
},
"peerDependenciesMeta": {
"@rolldown/plugin-babel": {
"optional": true
},
"babel-plugin-react-compiler": {
"optional": true
}
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"devOptional": true,
"license": "MIT"
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/immer": {
"version": "11.1.4",
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz",
"integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
},
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-android-arm64": "1.32.0",
"lightningcss-darwin-arm64": "1.32.0",
"lightningcss-darwin-x64": "1.32.0",
"lightningcss-freebsd-x64": "1.32.0",
"lightningcss-linux-arm-gnueabihf": "1.32.0",
"lightningcss-linux-arm64-gnu": "1.32.0",
"lightningcss-linux-arm64-musl": "1.32.0",
"lightningcss-linux-x64-gnu": "1.32.0",
"lightningcss-linux-x64-musl": "1.32.0",
"lightningcss-win32-arm64-msvc": "1.32.0",
"lightningcss-win32-x64-msvc": "1.32.0"
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postcss": {
"version": "8.5.10",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/react": {
"version": "19.2.5",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
"integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.2.5",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
"integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
"license": "MIT",
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
"react": "^19.2.5"
}
},
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT",
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
"@types/react": "^18.2.25 || ^19",
"react": "^18.0 || ^19",
"redux": "^5.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"redux": {
"optional": true
}
}
},
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT"
},
"node_modules/redux-thunk": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
"license": "MIT",
"peerDependencies": {
"redux": "^5.0.0"
}
},
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
"license": "MIT"
},
"node_modules/rolldown": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz",
"integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.124.0",
"@rolldown/pluginutils": "1.0.0-rc.15"
},
"bin": {
"rolldown": "bin/cli.mjs"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.0-rc.15",
"@rolldown/binding-darwin-arm64": "1.0.0-rc.15",
"@rolldown/binding-darwin-x64": "1.0.0-rc.15",
"@rolldown/binding-freebsd-x64": "1.0.0-rc.15",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.15",
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.15",
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.15",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15",
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15"
}
},
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz",
"integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==",
"dev": true,
"license": "MIT"
},
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT"
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tinyglobby": {
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/typescript": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz",
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/vite": {
"version": "8.0.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
"integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
"postcss": "^8.5.8",
"rolldown": "1.0.0-rc.15",
"tinyglobby": "^0.2.15"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
"@vitejs/devtools": "^0.1.0",
"esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
"sass": "^1.70.0",
"sass-embedded": "^1.70.0",
"stylus": ">=0.54.8",
"sugarss": "^5.0.0",
"terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"@vitejs/devtools": {
"optional": true
},
"esbuild": {
"optional": true
},
"jiti": {
"optional": true
},
"less": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
}
}
}
}
}

22
frontend/node_modules/@oxc-project/types/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2024-present VoidZero Inc. & Contributors
Copyright (c) 2023 Boshen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

3
frontend/node_modules/@oxc-project/types/README.md generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# Oxc Types
Typescript definitions for Oxc AST nodes.

26
frontend/node_modules/@oxc-project/types/package.json generated vendored Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "@oxc-project/types",
"version": "0.124.0",
"description": "Types for Oxc AST nodes",
"keywords": [
"AST",
"Parser"
],
"homepage": "https://oxc.rs",
"bugs": "https://github.com/oxc-project/oxc/issues",
"license": "MIT",
"author": "Boshen and oxc contributors",
"repository": {
"type": "git",
"url": "git+https://github.com/oxc-project/oxc.git",
"directory": "npm/oxc-types"
},
"funding": {
"url": "https://github.com/sponsors/Boshen"
},
"files": [
"types.d.ts"
],
"type": "module",
"types": "types.d.ts"
}

1912
frontend/node_modules/@oxc-project/types/types.d.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

21
frontend/node_modules/@reduxjs/toolkit/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Mark Erikson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

110
frontend/node_modules/@reduxjs/toolkit/README.md generated vendored Normal file
View File

@@ -0,0 +1,110 @@
# Redux Toolkit
![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/reduxjs/redux-toolkit/tests.yml?style=flat-square)
[![npm version](https://img.shields.io/npm/v/@reduxjs/toolkit.svg?style=flat-square)](https://www.npmjs.com/package/@reduxjs/toolkit)
[![npm downloads](https://img.shields.io/npm/dm/@reduxjs/toolkit.svg?style=flat-square&label=RTK+downloads)](https://www.npmjs.com/package/@reduxjs/toolkit)
**The official, opinionated, batteries-included toolset for efficient Redux development**
## Installation
### Create a React Redux App
The recommended way to start new apps with React and Redux Toolkit is by using [our official Redux Toolkit + TS template for Vite](https://github.com/reduxjs/redux-templates), or by creating a new Next.js project using [Next's `with-redux` template](https://github.com/vercel/next.js/tree/canary/examples/with-redux).
Both of these already have Redux Toolkit and React-Redux configured appropriately for that build tool, and come with a small example app that demonstrates how to use several of Redux Toolkit's features.
```bash
# Vite with our Redux+TS template
# (using the `degit` tool to clone and extract the template)
npx degit reduxjs/redux-templates/packages/vite-template-redux my-app
# Next.js using the `with-redux` template
npx create-next-app --example with-redux my-app
```
We do not currently have official React Native templates, but recommend these templates for standard React Native and for Expo:
- https://github.com/rahsheen/react-native-template-redux-typescript
- https://github.com/rahsheen/expo-template-redux-typescript
### An Existing App
Redux Toolkit is available as a package on NPM for use with a module bundler or in a Node application:
```bash
# NPM
npm install @reduxjs/toolkit
# Yarn
yarn add @reduxjs/toolkit
```
The package includes a precompiled ESM build that can be used as a [`<script type="module">` tag](https://unpkg.com/@reduxjs/toolkit/dist/redux-toolkit.browser.mjs) directly in the browser.
## Documentation
The Redux Toolkit docs are available at **https://redux-toolkit.js.org**, including API references and usage guides for all of the APIs included in Redux Toolkit.
The Redux core docs at https://redux.js.org includes the full Redux tutorials, as well usage guides on general Redux patterns.
## Purpose
The **Redux Toolkit** package is intended to be the standard way to write Redux logic. It was originally created to help address three common concerns about Redux:
- "Configuring a Redux store is too complicated"
- "I have to add a lot of packages to get Redux to do anything useful"
- "Redux requires too much boilerplate code"
We can't solve every use case, but in the spirit of [`create-react-app`](https://github.com/facebook/create-react-app), we can try to provide some tools that abstract over the setup process and handle the most common use cases, as well as include some useful utilities that will let the user simplify their application code.
Because of that, this package is deliberately limited in scope. It does _not_ address concepts like "reusable encapsulated Redux modules", folder or file structures, managing entity relationships in the store, and so on.
Redux Toolkit also includes a powerful data fetching and caching capability that we've dubbed "RTK Query". It's included in the package as a separate set of entry points. It's optional, but can eliminate the need to hand-write data fetching logic yourself.
## What's Included
Redux Toolkit includes these APIs:
- `configureStore()`: wraps `createStore` to provide simplified configuration options and good defaults. It can automatically combine your slice reducers, add whatever Redux middleware you supply, includes `redux-thunk` by default, and enables use of the Redux DevTools Extension.
- `createReducer()`: lets you supply a lookup table of action types to case reducer functions, rather than writing switch statements. In addition, it automatically uses the [`immer` library](https://github.com/mweststrate/immer) to let you write simpler immutable updates with normal mutative code, like `state.todos[3].completed = true`.
- `createAction()`: generates an action creator function for the given action type string. The function itself has `toString()` defined, so that it can be used in place of the type constant.
- `createSlice()`: combines `createReducer()` + `createAction()`. Accepts an object of reducer functions, a slice name, and an initial state value, and automatically generates a slice reducer with corresponding action creators and action types.
- `combineSlices()`: combines multiple slices into a single reducer, and allows "lazy loading" of slices after initialisation.
- `createListenerMiddleware()`: lets you define "listener" entries that contain an "effect" callback with additional logic, and a way to specify when that callback should run based on dispatched actions or state changes. A lightweight alternative to Redux async middleware like sagas and observables.
- `createAsyncThunk()`: accepts an action type string and a function that returns a promise, and generates a thunk that dispatches `pending/resolved/rejected` action types based on that promise
- `createEntityAdapter()`: generates a set of reusable reducers and selectors to manage normalized data in the store
- The `createSelector()` utility from the [Reselect](https://github.com/reduxjs/reselect) library, re-exported for ease of use.
For details, see [the Redux Toolkit API Reference section in the docs](https://redux-toolkit.js.org/api/configureStore).
## RTK Query
**RTK Query** is provided as an optional addon within the `@reduxjs/toolkit` package. It is purpose-built to solve the use case of data fetching and caching, supplying a compact, but powerful toolset to define an API interface layer for your app. It is intended to simplify common cases for loading data in a web application, eliminating the need to hand-write data fetching & caching logic yourself.
RTK Query is built on top of the Redux Toolkit core for its implementation, using [Redux](https://redux.js.org/) internally for its architecture. Although knowledge of Redux and RTK are not required to use RTK Query, you should explore all of the additional global store management capabilities they provide, as well as installing the [Redux DevTools browser extension](https://github.com/reduxjs/redux-devtools), which works flawlessly with RTK Query to traverse and replay a timeline of your request & cache behavior.
RTK Query is included within the installation of the core Redux Toolkit package. It is available via either of the two entry points below:
```ts no-transpile
import { createApi } from '@reduxjs/toolkit/query'
/* React-specific entry point that automatically generates
hooks corresponding to the defined endpoints */
import { createApi } from '@reduxjs/toolkit/query/react'
```
### What's included
RTK Query includes these APIs:
- `createApi()`: The core of RTK Query's functionality. It allows you to define a set of endpoints describe how to retrieve data from a series of endpoints, including configuration of how to fetch and transform that data. In most cases, you should use this once per app, with "one API slice per base URL" as a rule of thumb.
- `fetchBaseQuery()`: A small wrapper around fetch that aims to simplify requests. Intended as the recommended baseQuery to be used in createApi for the majority of users.
- `<ApiProvider />`: Can be used as a Provider if you do not already have a Redux store.
- `setupListeners()`: A utility used to enable refetchOnMount and refetchOnReconnect behaviors.
See the [**RTK Query Overview**](https://redux-toolkit.js.org/rtk-query/overview) page for more details on what RTK Query is, what problems it solves, and how to use it.
## Contributing
Please refer to our [contributing guide](/CONTRIBUTING.md) to learn about our development process, how to propose bugfixes and improvements, and how to build and test your changes to Redux Toolkit.

View File

@@ -0,0 +1,6 @@
'use strict'
if (process.env.NODE_ENV === 'production') {
module.exports = require('./redux-toolkit.production.min.cjs')
} else {
module.exports = require('./redux-toolkit.development.cjs')
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2662
frontend/node_modules/@reduxjs/toolkit/dist/index.d.mts generated vendored Normal file

File diff suppressed because it is too large Load Diff

2662
frontend/node_modules/@reduxjs/toolkit/dist/index.d.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
'use strict'
if (process.env.NODE_ENV === 'production') {
module.exports = require('./rtk-query.production.min.cjs')
} else {
module.exports = require('./rtk-query.development.cjs')
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
'use strict'
if (process.env.NODE_ENV === 'production') {
module.exports = require('./rtk-query-react.production.min.cjs')
} else {
module.exports = require('./rtk-query-react.development.cjs')
}

View File

@@ -0,0 +1,748 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/query/react/index.ts
var react_exports = {};
__export(react_exports, {
ApiProvider: () => ApiProvider,
UNINITIALIZED_VALUE: () => UNINITIALIZED_VALUE,
createApi: () => createApi,
reactHooksModule: () => reactHooksModule,
reactHooksModuleName: () => reactHooksModuleName
});
module.exports = __toCommonJS(react_exports);
// src/query/react/rtkqImports.ts
var import_query = require("@reduxjs/toolkit/query");
// src/query/react/module.ts
var import_toolkit2 = require("@reduxjs/toolkit");
var import_react_redux2 = require("react-redux");
var import_reselect = require("reselect");
// src/query/utils/capitalize.ts
function capitalize(str) {
return str.replace(str[0], str[0].toUpperCase());
}
// src/query/utils/countObjectKeys.ts
function countObjectKeys(obj) {
let count = 0;
for (const _key in obj) {
count++;
}
return count;
}
// src/query/endpointDefinitions.ts
var ENDPOINT_QUERY = "query" /* query */;
var ENDPOINT_MUTATION = "mutation" /* mutation */;
var ENDPOINT_INFINITEQUERY = "infinitequery" /* infinitequery */;
function isQueryDefinition(e) {
return e.type === ENDPOINT_QUERY;
}
function isMutationDefinition(e) {
return e.type === ENDPOINT_MUTATION;
}
function isInfiniteQueryDefinition(e) {
return e.type === ENDPOINT_INFINITEQUERY;
}
// src/query/tsHelpers.ts
function safeAssign(target, ...args) {
return Object.assign(target, ...args);
}
// src/query/react/buildHooks.ts
var import_toolkit = require("@reduxjs/toolkit");
// src/query/react/reactImports.ts
var import_react = require("react");
// src/query/react/reactReduxImports.ts
var import_react_redux = require("react-redux");
// src/query/react/constants.ts
var UNINITIALIZED_VALUE = Symbol();
// src/query/react/useSerializedStableValue.ts
function useStableQueryArgs(queryArgs) {
const cache = (0, import_react.useRef)(queryArgs);
const copy = (0, import_react.useMemo)(() => (0, import_query.copyWithStructuralSharing)(cache.current, queryArgs), [queryArgs]);
(0, import_react.useEffect)(() => {
if (cache.current !== copy) {
cache.current = copy;
}
}, [copy]);
return copy;
}
// src/query/react/useShallowStableValue.ts
function useShallowStableValue(value) {
const cache = (0, import_react.useRef)(value);
(0, import_react.useEffect)(() => {
if (!(0, import_react_redux.shallowEqual)(cache.current, value)) {
cache.current = value;
}
}, [value]);
return (0, import_react_redux.shallowEqual)(cache.current, value) ? cache.current : value;
}
// src/query/react/buildHooks.ts
var canUseDOM = () => !!(typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined");
var isDOM = /* @__PURE__ */ canUseDOM();
var isRunningInReactNative = () => typeof navigator !== "undefined" && navigator.product === "ReactNative";
var isReactNative = /* @__PURE__ */ isRunningInReactNative();
var getUseIsomorphicLayoutEffect = () => isDOM || isReactNative ? import_react.useLayoutEffect : import_react.useEffect;
var useIsomorphicLayoutEffect = /* @__PURE__ */ getUseIsomorphicLayoutEffect();
var noPendingQueryStateSelector = (selected) => {
if (selected.isUninitialized) {
return {
...selected,
isUninitialized: false,
isFetching: true,
isLoading: selected.data !== void 0 ? false : true,
// This is the one place where we still have to use `QueryStatus` as an enum,
// since it's the only reference in the React package and not in the core.
status: import_query.QueryStatus.pending
};
}
return selected;
};
function pick(obj, ...keys) {
const ret = {};
keys.forEach((key) => {
ret[key] = obj[key];
});
return ret;
}
var COMMON_HOOK_DEBUG_FIELDS = ["data", "status", "isLoading", "isSuccess", "isError", "error"];
function buildHooks({
api,
moduleOptions: {
batch,
hooks: {
useDispatch,
useSelector,
useStore
},
unstable__sideEffectsInRender,
createSelector
},
serializeQueryArgs,
context
}) {
const usePossiblyImmediateEffect = unstable__sideEffectsInRender ? (cb) => cb() : import_react.useEffect;
const unsubscribePromiseRef = (ref) => ref.current?.unsubscribe?.();
const endpointDefinitions = context.endpointDefinitions;
return {
buildQueryHooks,
buildInfiniteQueryHooks,
buildMutationHook,
usePrefetch
};
function queryStatePreSelector(currentState, lastResult, queryArgs) {
if (lastResult?.endpointName && currentState.isUninitialized) {
const {
endpointName
} = lastResult;
const endpointDefinition = endpointDefinitions[endpointName];
if (queryArgs !== import_query.skipToken && serializeQueryArgs({
queryArgs: lastResult.originalArgs,
endpointDefinition,
endpointName
}) === serializeQueryArgs({
queryArgs,
endpointDefinition,
endpointName
})) lastResult = void 0;
}
let data = currentState.isSuccess ? currentState.data : lastResult?.data;
if (data === void 0) data = currentState.data;
const hasData = data !== void 0;
const isFetching = currentState.isLoading;
const isLoading = (!lastResult || lastResult.isLoading || lastResult.isUninitialized) && !hasData && isFetching;
const isSuccess = currentState.isSuccess || hasData && (isFetching && !lastResult?.isError || currentState.isUninitialized);
return {
...currentState,
data,
currentData: currentState.data,
isFetching,
isLoading,
isSuccess
};
}
function infiniteQueryStatePreSelector(currentState, lastResult, queryArgs) {
if (lastResult?.endpointName && currentState.isUninitialized) {
const {
endpointName
} = lastResult;
const endpointDefinition = endpointDefinitions[endpointName];
if (queryArgs !== import_query.skipToken && serializeQueryArgs({
queryArgs: lastResult.originalArgs,
endpointDefinition,
endpointName
}) === serializeQueryArgs({
queryArgs,
endpointDefinition,
endpointName
})) lastResult = void 0;
}
let data = currentState.isSuccess ? currentState.data : lastResult?.data;
if (data === void 0) data = currentState.data;
const hasData = data !== void 0;
const isFetching = currentState.isLoading;
const isLoading = (!lastResult || lastResult.isLoading || lastResult.isUninitialized) && !hasData && isFetching;
const isSuccess = currentState.isSuccess || isFetching && hasData;
return {
...currentState,
data,
currentData: currentState.data,
isFetching,
isLoading,
isSuccess
};
}
function usePrefetch(endpointName, defaultOptions) {
const dispatch = useDispatch();
const stableDefaultOptions = useShallowStableValue(defaultOptions);
return (0, import_react.useCallback)((arg, options) => dispatch(api.util.prefetch(endpointName, arg, {
...stableDefaultOptions,
...options
})), [endpointName, dispatch, stableDefaultOptions]);
}
function useQuerySubscriptionCommonImpl(endpointName, arg, {
refetchOnReconnect,
refetchOnFocus,
refetchOnMountOrArgChange,
skip = false,
pollingInterval = 0,
skipPollingIfUnfocused = false,
...rest
} = {}) {
const {
initiate
} = api.endpoints[endpointName];
const dispatch = useDispatch();
const subscriptionSelectorsRef = (0, import_react.useRef)(void 0);
if (!subscriptionSelectorsRef.current) {
const returnedValue = dispatch(api.internalActions.internal_getRTKQSubscriptions());
if (true) {
if (typeof returnedValue !== "object" || typeof returnedValue?.type === "string") {
throw new Error(false ? _formatProdErrorMessage(37) : `Warning: Middleware for RTK-Query API at reducerPath "${api.reducerPath}" has not been added to the store.
You must add the middleware for RTK-Query to function correctly!`);
}
}
subscriptionSelectorsRef.current = returnedValue;
}
const stableArg = useStableQueryArgs(skip ? import_query.skipToken : arg);
const stableSubscriptionOptions = useShallowStableValue({
refetchOnReconnect,
refetchOnFocus,
pollingInterval,
skipPollingIfUnfocused
});
const initialPageParam = rest.initialPageParam;
const stableInitialPageParam = useShallowStableValue(initialPageParam);
const refetchCachedPages = rest.refetchCachedPages;
const stableRefetchCachedPages = useShallowStableValue(refetchCachedPages);
const promiseRef = (0, import_react.useRef)(void 0);
let {
queryCacheKey,
requestId
} = promiseRef.current || {};
let currentRenderHasSubscription = false;
if (queryCacheKey && requestId) {
currentRenderHasSubscription = subscriptionSelectorsRef.current.isRequestSubscribed(queryCacheKey, requestId);
}
const subscriptionRemoved = !currentRenderHasSubscription && promiseRef.current !== void 0;
usePossiblyImmediateEffect(() => {
if (subscriptionRemoved) {
promiseRef.current = void 0;
}
}, [subscriptionRemoved]);
usePossiblyImmediateEffect(() => {
const lastPromise = promiseRef.current;
if (typeof process !== "undefined" && false) {
console.log(subscriptionRemoved);
}
if (stableArg === import_query.skipToken) {
lastPromise?.unsubscribe();
promiseRef.current = void 0;
return;
}
const lastSubscriptionOptions = promiseRef.current?.subscriptionOptions;
if (!lastPromise || lastPromise.arg !== stableArg) {
lastPromise?.unsubscribe();
const promise = dispatch(initiate(stableArg, {
subscriptionOptions: stableSubscriptionOptions,
forceRefetch: refetchOnMountOrArgChange,
...isInfiniteQueryDefinition(endpointDefinitions[endpointName]) ? {
initialPageParam: stableInitialPageParam,
refetchCachedPages: stableRefetchCachedPages
} : {}
}));
promiseRef.current = promise;
} else if (stableSubscriptionOptions !== lastSubscriptionOptions) {
lastPromise.updateSubscriptionOptions(stableSubscriptionOptions);
}
}, [dispatch, initiate, refetchOnMountOrArgChange, stableArg, stableSubscriptionOptions, subscriptionRemoved, stableInitialPageParam, stableRefetchCachedPages, endpointName]);
return [promiseRef, dispatch, initiate, stableSubscriptionOptions];
}
function buildUseQueryState(endpointName, preSelector) {
const useQueryState = (arg, {
skip = false,
selectFromResult
} = {}) => {
const {
select
} = api.endpoints[endpointName];
const stableArg = useStableQueryArgs(skip ? import_query.skipToken : arg);
const lastValue = (0, import_react.useRef)(void 0);
const selectDefaultResult = (0, import_react.useMemo)(() => (
// Normally ts-ignores are bad and should be avoided, but we're
// already casting this selector to be `Selector<any>` anyway,
// so the inconsistencies don't matter here
// @ts-ignore
createSelector([
// @ts-ignore
select(stableArg),
(_, lastResult) => lastResult,
(_) => stableArg
], preSelector, {
memoizeOptions: {
resultEqualityCheck: import_react_redux.shallowEqual
}
})
), [select, stableArg]);
const querySelector = (0, import_react.useMemo)(() => selectFromResult ? createSelector([selectDefaultResult], selectFromResult, {
devModeChecks: {
identityFunctionCheck: "never"
}
}) : selectDefaultResult, [selectDefaultResult, selectFromResult]);
const currentState = useSelector((state) => querySelector(state, lastValue.current), import_react_redux.shallowEqual);
const store = useStore();
const newLastValue = selectDefaultResult(store.getState(), lastValue.current);
useIsomorphicLayoutEffect(() => {
lastValue.current = newLastValue;
}, [newLastValue]);
return currentState;
};
return useQueryState;
}
function usePromiseRefUnsubscribeOnUnmount(promiseRef) {
(0, import_react.useEffect)(() => {
return () => {
unsubscribePromiseRef(promiseRef);
promiseRef.current = void 0;
};
}, [promiseRef]);
}
function refetchOrErrorIfUnmounted(promiseRef) {
if (!promiseRef.current) throw new Error(false ? _formatProdErrorMessage2(38) : "Cannot refetch a query that has not been started yet.");
return promiseRef.current.refetch();
}
function buildQueryHooks(endpointName) {
const useQuerySubscription = (arg, options = {}) => {
const [promiseRef] = useQuerySubscriptionCommonImpl(endpointName, arg, options);
usePromiseRefUnsubscribeOnUnmount(promiseRef);
return (0, import_react.useMemo)(() => ({
/**
* A method to manually refetch data for the query
*/
refetch: () => refetchOrErrorIfUnmounted(promiseRef)
}), [promiseRef]);
};
const useLazyQuerySubscription = ({
refetchOnReconnect,
refetchOnFocus,
pollingInterval = 0,
skipPollingIfUnfocused = false
} = {}) => {
const {
initiate
} = api.endpoints[endpointName];
const dispatch = useDispatch();
const [arg, setArg] = (0, import_react.useState)(UNINITIALIZED_VALUE);
const promiseRef = (0, import_react.useRef)(void 0);
const stableSubscriptionOptions = useShallowStableValue({
refetchOnReconnect,
refetchOnFocus,
pollingInterval,
skipPollingIfUnfocused
});
usePossiblyImmediateEffect(() => {
const lastSubscriptionOptions = promiseRef.current?.subscriptionOptions;
if (stableSubscriptionOptions !== lastSubscriptionOptions) {
promiseRef.current?.updateSubscriptionOptions(stableSubscriptionOptions);
}
}, [stableSubscriptionOptions]);
const subscriptionOptionsRef = (0, import_react.useRef)(stableSubscriptionOptions);
usePossiblyImmediateEffect(() => {
subscriptionOptionsRef.current = stableSubscriptionOptions;
}, [stableSubscriptionOptions]);
const trigger = (0, import_react.useCallback)(function(arg2, preferCacheValue = false) {
let promise;
batch(() => {
unsubscribePromiseRef(promiseRef);
promiseRef.current = promise = dispatch(initiate(arg2, {
subscriptionOptions: subscriptionOptionsRef.current,
forceRefetch: !preferCacheValue
}));
setArg(arg2);
});
return promise;
}, [dispatch, initiate]);
const reset = (0, import_react.useCallback)(() => {
if (promiseRef.current?.queryCacheKey) {
dispatch(api.internalActions.removeQueryResult({
queryCacheKey: promiseRef.current?.queryCacheKey
}));
}
}, [dispatch]);
(0, import_react.useEffect)(() => {
return () => {
unsubscribePromiseRef(promiseRef);
};
}, []);
(0, import_react.useEffect)(() => {
if (arg !== UNINITIALIZED_VALUE && !promiseRef.current) {
trigger(arg, true);
}
}, [arg, trigger]);
return (0, import_react.useMemo)(() => [trigger, arg, {
reset
}], [trigger, arg, reset]);
};
const useQueryState = buildUseQueryState(endpointName, queryStatePreSelector);
return {
useQueryState,
useQuerySubscription,
useLazyQuerySubscription,
useLazyQuery(options) {
const [trigger, arg, {
reset
}] = useLazyQuerySubscription(options);
const queryStateResults = useQueryState(arg, {
...options,
skip: arg === UNINITIALIZED_VALUE
});
const info = (0, import_react.useMemo)(() => ({
lastArg: arg
}), [arg]);
return (0, import_react.useMemo)(() => [trigger, {
...queryStateResults,
reset
}, info], [trigger, queryStateResults, reset, info]);
},
useQuery(arg, options) {
const querySubscriptionResults = useQuerySubscription(arg, options);
const queryStateResults = useQueryState(arg, {
selectFromResult: arg === import_query.skipToken || options?.skip ? void 0 : noPendingQueryStateSelector,
...options
});
const debugValue = pick(queryStateResults, ...COMMON_HOOK_DEBUG_FIELDS);
(0, import_react.useDebugValue)(debugValue);
return (0, import_react.useMemo)(() => ({
...queryStateResults,
...querySubscriptionResults
}), [queryStateResults, querySubscriptionResults]);
}
};
}
function buildInfiniteQueryHooks(endpointName) {
const useInfiniteQuerySubscription = (arg, options = {}) => {
const [promiseRef, dispatch, initiate, stableSubscriptionOptions] = useQuerySubscriptionCommonImpl(endpointName, arg, options);
const subscriptionOptionsRef = (0, import_react.useRef)(stableSubscriptionOptions);
usePossiblyImmediateEffect(() => {
subscriptionOptionsRef.current = stableSubscriptionOptions;
}, [stableSubscriptionOptions]);
const hookRefetchCachedPages = options.refetchCachedPages;
const stableHookRefetchCachedPages = useShallowStableValue(hookRefetchCachedPages);
const trigger = (0, import_react.useCallback)(function(arg2, direction) {
let promise;
batch(() => {
unsubscribePromiseRef(promiseRef);
promiseRef.current = promise = dispatch(initiate(arg2, {
subscriptionOptions: subscriptionOptionsRef.current,
direction
}));
});
return promise;
}, [promiseRef, dispatch, initiate]);
usePromiseRefUnsubscribeOnUnmount(promiseRef);
const stableArg = useStableQueryArgs(options.skip ? import_query.skipToken : arg);
const refetch = (0, import_react.useCallback)((options2) => {
if (!promiseRef.current) throw new Error(false ? _formatProdErrorMessage3(38) : "Cannot refetch a query that has not been started yet.");
const mergedOptions = {
refetchCachedPages: options2?.refetchCachedPages ?? stableHookRefetchCachedPages
};
return promiseRef.current.refetch(mergedOptions);
}, [promiseRef, stableHookRefetchCachedPages]);
return (0, import_react.useMemo)(() => {
const fetchNextPage = () => {
return trigger(stableArg, "forward");
};
const fetchPreviousPage = () => {
return trigger(stableArg, "backward");
};
return {
trigger,
/**
* A method to manually refetch data for the query
*/
refetch,
fetchNextPage,
fetchPreviousPage
};
}, [refetch, trigger, stableArg]);
};
const useInfiniteQueryState = buildUseQueryState(endpointName, infiniteQueryStatePreSelector);
return {
useInfiniteQueryState,
useInfiniteQuerySubscription,
useInfiniteQuery(arg, options) {
const {
refetch,
fetchNextPage,
fetchPreviousPage
} = useInfiniteQuerySubscription(arg, options);
const queryStateResults = useInfiniteQueryState(arg, {
selectFromResult: arg === import_query.skipToken || options?.skip ? void 0 : noPendingQueryStateSelector,
...options
});
const debugValue = pick(queryStateResults, ...COMMON_HOOK_DEBUG_FIELDS, "hasNextPage", "hasPreviousPage");
(0, import_react.useDebugValue)(debugValue);
return (0, import_react.useMemo)(() => ({
...queryStateResults,
fetchNextPage,
fetchPreviousPage,
refetch
}), [queryStateResults, fetchNextPage, fetchPreviousPage, refetch]);
}
};
}
function buildMutationHook(name) {
return ({
selectFromResult,
fixedCacheKey
} = {}) => {
const {
select,
initiate
} = api.endpoints[name];
const dispatch = useDispatch();
const [promise, setPromise] = (0, import_react.useState)();
(0, import_react.useEffect)(() => () => {
if (!promise?.arg.fixedCacheKey) {
promise?.reset();
}
}, [promise]);
const triggerMutation = (0, import_react.useCallback)(function(arg) {
const promise2 = dispatch(initiate(arg, {
fixedCacheKey
}));
setPromise(promise2);
return promise2;
}, [dispatch, initiate, fixedCacheKey]);
const {
requestId
} = promise || {};
const selectDefaultResult = (0, import_react.useMemo)(() => select({
fixedCacheKey,
requestId: promise?.requestId
}), [fixedCacheKey, promise, select]);
const mutationSelector = (0, import_react.useMemo)(() => selectFromResult ? createSelector([selectDefaultResult], selectFromResult) : selectDefaultResult, [selectFromResult, selectDefaultResult]);
const currentState = useSelector(mutationSelector, import_react_redux.shallowEqual);
const originalArgs = fixedCacheKey == null ? promise?.arg.originalArgs : void 0;
const reset = (0, import_react.useCallback)(() => {
batch(() => {
if (promise) {
setPromise(void 0);
}
if (fixedCacheKey) {
dispatch(api.internalActions.removeMutationResult({
requestId,
fixedCacheKey
}));
}
});
}, [dispatch, fixedCacheKey, promise, requestId]);
const debugValue = pick(currentState, ...COMMON_HOOK_DEBUG_FIELDS, "endpointName");
(0, import_react.useDebugValue)(debugValue);
const finalState = (0, import_react.useMemo)(() => ({
...currentState,
originalArgs,
reset
}), [currentState, originalArgs, reset]);
return (0, import_react.useMemo)(() => [triggerMutation, finalState], [triggerMutation, finalState]);
};
}
}
// src/query/react/module.ts
var reactHooksModuleName = /* @__PURE__ */ Symbol();
var reactHooksModule = ({
batch = import_react_redux2.batch,
hooks = {
useDispatch: import_react_redux2.useDispatch,
useSelector: import_react_redux2.useSelector,
useStore: import_react_redux2.useStore
},
createSelector = import_reselect.createSelector,
unstable__sideEffectsInRender = false,
...rest
} = {}) => {
if (true) {
const hookNames = ["useDispatch", "useSelector", "useStore"];
let warned = false;
for (const hookName of hookNames) {
if (countObjectKeys(rest) > 0) {
if (rest[hookName]) {
if (!warned) {
console.warn("As of RTK 2.0, the hooks now need to be specified as one object, provided under a `hooks` key:\n`reactHooksModule({ hooks: { useDispatch, useSelector, useStore } })`");
warned = true;
}
}
hooks[hookName] = rest[hookName];
}
if (typeof hooks[hookName] !== "function") {
throw new Error(false ? _formatProdErrorMessage4(36) : `When using custom hooks for context, all ${hookNames.length} hooks need to be provided: ${hookNames.join(", ")}.
Hook ${hookName} was either not provided or not a function.`);
}
}
}
return {
name: reactHooksModuleName,
init(api, {
serializeQueryArgs
}, context) {
const anyApi = api;
const {
buildQueryHooks,
buildInfiniteQueryHooks,
buildMutationHook,
usePrefetch
} = buildHooks({
api,
moduleOptions: {
batch,
hooks,
unstable__sideEffectsInRender,
createSelector
},
serializeQueryArgs,
context
});
safeAssign(anyApi, {
usePrefetch
});
safeAssign(context, {
batch
});
return {
injectEndpoint(endpointName, definition) {
if (isQueryDefinition(definition)) {
const {
useQuery,
useLazyQuery,
useLazyQuerySubscription,
useQueryState,
useQuerySubscription
} = buildQueryHooks(endpointName);
safeAssign(anyApi.endpoints[endpointName], {
useQuery,
useLazyQuery,
useLazyQuerySubscription,
useQueryState,
useQuerySubscription
});
api[`use${capitalize(endpointName)}Query`] = useQuery;
api[`useLazy${capitalize(endpointName)}Query`] = useLazyQuery;
}
if (isMutationDefinition(definition)) {
const useMutation = buildMutationHook(endpointName);
safeAssign(anyApi.endpoints[endpointName], {
useMutation
});
api[`use${capitalize(endpointName)}Mutation`] = useMutation;
} else if (isInfiniteQueryDefinition(definition)) {
const {
useInfiniteQuery,
useInfiniteQuerySubscription,
useInfiniteQueryState
} = buildInfiniteQueryHooks(endpointName);
safeAssign(anyApi.endpoints[endpointName], {
useInfiniteQuery,
useInfiniteQuerySubscription,
useInfiniteQueryState
});
api[`use${capitalize(endpointName)}InfiniteQuery`] = useInfiniteQuery;
}
}
};
}
};
};
// src/query/react/index.ts
__reExport(react_exports, require("@reduxjs/toolkit/query"), module.exports);
// src/query/react/ApiProvider.tsx
var import_toolkit3 = require("@reduxjs/toolkit");
var React = __toESM(require("react"));
function ApiProvider(props) {
const context = props.context || import_react_redux.ReactReduxContext;
const existingContext = (0, import_react.useContext)(context);
if (existingContext) {
throw new Error(false ? _formatProdErrorMessage5(35) : "Existing Redux context detected. If you already have a store set up, please use the traditional Redux setup.");
}
const [store] = React.useState(() => (0, import_toolkit3.configureStore)({
reducer: {
[props.api.reducerPath]: props.api.reducer
},
middleware: (gDM) => gDM().concat(props.api.middleware)
}));
(0, import_react.useEffect)(() => props.setupListeners === false ? void 0 : (0, import_query.setupListeners)(store.dispatch, props.setupListeners), [props.setupListeners, store.dispatch]);
return /* @__PURE__ */ React.createElement(import_react_redux.Provider, { store, context }, props.children);
}
// src/query/react/index.ts
var createApi = /* @__PURE__ */ (0, import_query.buildCreateApi)((0, import_query.coreModule)(), reactHooksModule());
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
ApiProvider,
UNINITIALIZED_VALUE,
createApi,
reactHooksModule,
reactHooksModuleName,
...require("@reduxjs/toolkit/query")
});
//# sourceMappingURL=rtk-query-react.development.cjs.map

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More