En esta lección se muestra un ejemplo de sincronización de bases de datos.
Puedes probar el ejemplo en varios navegadores y dispositivos abriendo la url https://sincro.rf.gd/.
Las modificaciones que realices en dispositivo o navegador se verán reflejados en los otros dispositivos, en un máximo de 20 segundos.
Puedes trabajar sin conexión en algunos dispositivos y con conexión en otros. Si conectas todos los dispositivos, estos mostrarán los mismos datos después de un tiempo.
Comercialmente hay algunos productos como:
Prueba el ejemplo en https://sincro.rf.gd/.
Copia la url de la app y pégala en varios navegadores y dispositivos.
Las modificaciones que realices en dispositivo o navegador se verán reflejados en los otros dispositivos, en un máximo de 20 segundos.
Puedes trabajar sin conexión en algunos dispositivos y con conexión en otros. Si conectas todos los dispositivos, estos mostrarán los mismos datos después de un tiempo.
Descarga el archivo /src/sincro.zip y descompáctalo.
Crea una cuenta de email con el nombre de tu sitio, por ejemplo, miapp@google.com
Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo miapp.
Crea un repositorio nuevo. En el nombre del repositorio debes poner el nombre de tu cuenta seguido por .github.io; por ejemplo miapp.github.io
Edita los archivos que desees.
El archivo sw.js
tiene una lista de los archivos que se instalan.
El archivo instruccionesListadoSw.txt
te indica como generarla usando
Visual Studio Code.
Prueba tu sitio localmente.
Necesitas un hosting. En este ejemplo se muestra como usar el hosting. https://infinityfree.com/ Si no lo has usado, lo primero que tienes que hacer es entrar a registrar tu email con el botón Registrar. Si ya tienes tu email registrado, omite este paso.
Crea una cuenta. Si ya tienes cuenta, entra a ella y crea un nuevo dominio. En este ejemplo no se crean los archivos directamente en el hosting.
Sube tus archivos a GitHub. En este ejemplo no hay archivo sw.js ni necesitas esperar 11 o más minutos.
Prueba el ejemplo en https://sincro.rf.gd/.
Copia la url de la app y pégala en varios navegadores y dispositivos.
Las modificaciones que realices en dispositivo o navegador se verán reflejados en los otros dispositivos, en un máximo de 20 segundos.
Puedes trabajar sin conexión en algunos dispositivos y con conexión en otros. Si conectas todos los dispositivos, estos mostrarán los mismos datos después de un tiempo.
Descarga el archivo /src/sincro.zip y descompáctalo.
Crea tu proyecto en GitHub:
Crea una cuenta de email, por ejemplo, pepito@google.com
Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo pepito.
Crea un repositorio nuevo. En la página principal de GitHub cliquea 📘 New.
En la página Create a new repository introduce los siguientes datos:
Proporciona el nombre de tu repositorio debajo de donde dice Repository name *.
Mantén la selección Public para que otros usuarios puedan ver tu proyecto.
Verifica la casilla Add a README file. En este archivo se muestra información sobre tu proyecto.
Cliquea License: None. y selecciona la licencia que consideres más adecuada para tu proyecto.
Cliquea Create repository.
Importa el proyecto en GitHub:
En la página principal de tu proyecto en GitHub, en la pestaña < > Code, cliquea < > Code y en la sección Branches y copia la dirección que está en HTTPS, debajo de Clone.
En Visual Studio Code, usa el botón de la izquierda para Source Control.
Cliquea el botón Clone Repository.
Pega la url que copiaste anteriormente hasta arriba, donde dice algo como Provide repository URL y presiona la teclea Intro.
Selecciona la carpeta donde se guardará la carpeta del proyecto.
Abre la carpeta del proyecto importado.
Añade el contenido de la carpeta descompactada que contiene el código del ejemplo.
Edita los archivos que desees.
El archivo sw.js
tiene una lista de los archivos que se instalan.
El archivo instruccionesListadoSw.txt
te indica como generarla usando
Visual Studio Code.
Cada vez que modifiques los archivos, debes modificar el valor
de VERSION en el archivo sw.js
para poder ver los cambios
en el navegador.
Haz clic derecho en index.html
, selecciona
PHP Server: serve project y se abre el navegador para que puedas
probar localmente el ejemplo.
Cuando desarrolles, es incómodo modificar la versión cada que realizas cambios; en vez de ello desinstala la app:
Abre las herramientas de depuración haciendo clic derecho en la página y selecciona Inspeccionar (o Inspect si aparece en inglés).
En la Pestaña Aplicación (o Application en inglés) selecciona Almacenamoento (o Storage en inglés). Cliquea Borrar datos del sitio.
Recarga la app, de preferencia haciendo clic derecho en el ícono de
volver a cargar la página
y
seleccionando vaciar caché y volver a cargar de manera forzada (o
algo parecido). Si no aparece un menú emergente, simplemente cliquea
volver a cargar la página
.
Revisa que no aparezca ningún error ni en la pestañas Consola, ni
en Red.
Tanbién puedes usar la combinación de teclas Ctrl+Mayúsculas+r para forzar que se actualice temporalmente el navegador en caso de que no se vean los cambios.
En la Pestaña Aplicación (o Application en inglés) selecciona Almacenamiento en caché (o Cache storage en inglés). Aquí puedes revisar si el caché de la aplicación se llenó correctamente. En caso de que esté vacío, es que hubo algún error durante la carga y la app se ejecuta más lenta.
Para depurar paso a paso haz lo siguiente:
En el navegador, haz clic derecho en la página que deseas depurar y selecciona inspeccionar.
Selecciona la pestaña Fuentes (o Sources si tu navegador está en Inglés).
Selecciona el archivo donde vas a empezar a depurar.
Haz clic en el número de la línea donde vas a empezar a depurar.
En Visual Studio Code, abre el archivo de PHP donde vas a empezar a depurar.
Haz clic en Run and Debug
.
Si no está configurada la depuración, haz clic en create a launch json file.
Haz clic en la flechita RUN AND DEBUG, al lado de la cual debe
decir Listen for Xdebug
.
Aparece un cuadro con los controles de depuración
Selecciona otra vez el archivo de PHP y haz clic en el número de la línea donde vas a empezar a depurar.
Regresa al navegador, recarga la página de manera normal y empieza a usarla.
Si se ejecuta alguna de las líneas de código seleccionadas, aparece resaltada en la pestaña de fuentes. Usa los controles de depuración para avanzar, como se muestra en este video.
Sube el proyecto al hosting que elijas.
Crea una nueva carpeta para crear un nuevo proyecto que estará conectado directamente al servidor web por ftp.
Abre la nueva carpeta con Visual Studio Code.
Tecle al mismo Mayúsculas+Control+P y selecciona SFTP: Config. Aparece un archivo de configuración de FTP. Llena los datos con la configuración de FTP de tu servidor, excepto la contraseña.
Cliquea el botón de SFTP y luego haz clic en la URL de tu servidos. En la barra superior te pide la contraseña y ENTER.
Pásate a la parte de archivos y coloca tus archivos.
Cliquea con el botón derecho en la sección de archivos y selecciona Sync: Local -> Remote.
Abre un navegador y prueba el proyecto en tu hosting.
En el hosting InfinityFree, la primera vez que corres la página, puede marcar un mensaje de error, pero al recargar funciona correctamente. Puedes evitar este problema usando un dominio propio.
Para subir el código a GitHub, en la sección de SOURCE CONTROL, en Message introduce un mensaje sobre los cambios que hiciste, por ejemplo index.html corregido, selecciona v y luego Commit & Push.
Haz clic en los triángulos para expandir las carpetas
| 1 | <!DOCTYPE html> |
| 2 | <html lang="es"> |
| 3 | |
| 4 | <head> |
| 5 | |
| 6 | <meta charset="UTF-8"> |
| 7 | |
| 8 | <title>Sincronizacion</title> |
| 9 | |
| 10 | <meta name="viewport" content="width=device-width"> |
| 11 | |
| 12 | <script type="module" src="js/lib/registraServiceWorker.js"></script> |
| 13 | <script type="module" src="js/lib/manejaErrores.js"></script> |
| 14 | |
| 15 | </head> |
| 16 | |
| 17 | <body> |
| 18 | |
| 19 | <h1>Sincronizacion</h1> |
| 20 | |
| 21 | <p><a href="agrega.html">Agregar</a></p> |
| 22 | |
| 23 | <ul id="lista"> |
| 24 | <li><progress max="100">Cargando…</progress></li> |
| 25 | </ul> |
| 26 | |
| 27 | <script type="module"> |
| 28 | |
| 29 | import { muestraError } from "./js/lib/muestraError.js" |
| 30 | import { esperaUnPocoYSincroniza } from "./js/esperaUnPocoYSincroniza.js" |
| 31 | import { |
| 32 | pasatiempoConsultaNoEliminados |
| 33 | } from "./js/pasatiempoConsultaNoEliminados.js" |
| 34 | import { renderiza } from "./js/renderiza.js" |
| 35 | import { sincroniza } from "./js/sincroniza.js" |
| 36 | |
| 37 | pasatiempoVistaIndex() |
| 38 | |
| 39 | export async function pasatiempoVistaIndex() { |
| 40 | try { |
| 41 | const pasatiempos = await pasatiempoConsultaNoEliminados() |
| 42 | renderiza(pasatiempos) |
| 43 | sincroniza() |
| 44 | } catch (error) { |
| 45 | muestraError(error) |
| 46 | esperaUnPocoYSincroniza() |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | </script> |
| 51 | |
| 52 | </body> |
| 53 | |
| 54 | </html> |
| 1 | <!DOCTYPE html> |
| 2 | <html lang="es"> |
| 3 | |
| 4 | <head> |
| 5 | |
| 6 | <meta charset="UTF-8"> |
| 7 | <meta name="viewport" content="width=device-width"> |
| 8 | |
| 9 | <title>Agregar</title> |
| 10 | |
| 11 | <script type="module" src="js/lib/registraServiceWorker.js"></script> |
| 12 | <script type="module" src="js/lib/manejaErrores.js"></script> |
| 13 | |
| 14 | </head> |
| 15 | |
| 16 | <body> |
| 17 | |
| 18 | <form id="formulario"> |
| 19 | |
| 20 | <h1>Agregar</h1> |
| 21 | |
| 22 | <p><a href="index.html">Cancelar</a></p> |
| 23 | |
| 24 | <p> |
| 25 | <label> |
| 26 | Nombre * |
| 27 | <input name="nombre"> |
| 28 | </label> |
| 29 | </p> |
| 30 | <p>* Obligatorio</p> |
| 31 | <p><button type="submit">Agregar</button></p> |
| 32 | |
| 33 | </form> |
| 34 | |
| 35 | <script type="module"> |
| 36 | |
| 37 | import { pasatiempoAgrega } from "./js/pasatiempoAgrega.js" |
| 38 | |
| 39 | formulario.addEventListener("submit", pasatiempoAgrega) |
| 40 | |
| 41 | </script> |
| 42 | |
| 43 | </body> |
| 44 | |
| 45 | </html> |
| 1 | <!DOCTYPE html> |
| 2 | <html lang="es"> |
| 3 | |
| 4 | <head> |
| 5 | |
| 6 | <meta charset="UTF-8"> |
| 7 | <meta name="viewport" content="width=device-width"> |
| 8 | |
| 9 | <title>Modificar</title> |
| 10 | |
| 11 | <script type="module" src="js/lib/registraServiceWorker.js"></script> |
| 12 | <script type="module" src="js/lib/manejaErrores.js"></script> |
| 13 | |
| 14 | </head> |
| 15 | |
| 16 | <body> |
| 17 | |
| 18 | <form id="formulario"> |
| 19 | |
| 20 | <h1>Modificar</h1> |
| 21 | |
| 22 | <p><a href="index.html">Cancelar</a></p> |
| 23 | |
| 24 | <p> |
| 25 | <label> |
| 26 | Nombre * |
| 27 | <input name="nombre" value="Cargando…"> |
| 28 | </label> |
| 29 | </p> |
| 30 | |
| 31 | <p>* Obligatorio</p> |
| 32 | |
| 33 | <p> |
| 34 | |
| 35 | <button type="submit">Guardar</button> |
| 36 | |
| 37 | <button id="botonEliminar" type="button"> |
| 38 | Eliminar |
| 39 | </button> |
| 40 | |
| 41 | </p> |
| 42 | |
| 43 | </form> |
| 44 | |
| 45 | <script type="module"> |
| 46 | |
| 47 | import { |
| 48 | validaEntidadObligatoria |
| 49 | } from "./js/lib/validaEntidadObligatoria.js" |
| 50 | import { muestraObjeto } from "./js/lib/muestraObjeto.js" |
| 51 | import { pasatiempoBusca } from "./js/pasatiempoBusca.js" |
| 52 | import { pasatiempoElimina } from "./js/pasatiempoElimina.js" |
| 53 | import { pasatiempoModifica } from "./js/pasatiempoModifica.js" |
| 54 | |
| 55 | const params = new URL(location.href).searchParams |
| 56 | const id = params.get("id") |
| 57 | |
| 58 | descargaDatos() |
| 59 | |
| 60 | export async function descargaDatos() { |
| 61 | if (id !== null && id !== "") { |
| 62 | let modelo = await pasatiempoBusca(id) |
| 63 | modelo = validaEntidadObligatoria("Pasatiempo", modelo) |
| 64 | muestraObjeto(document, { nombre: { value: modelo.PAS_NOMBRE } }) |
| 65 | formulario |
| 66 | .addEventListener("submit", event => pasatiempoModifica(event, id)) |
| 67 | botonEliminar.addEventListener("click", () => pasatiempoElimina(id)) |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | </script> |
| 72 | |
| 73 | </body> |
| 74 | |
| 75 | </html> |
| 1 | Generar el listado de archivos del sw.js desde Visual Studio Code. |
| 2 | 1. Abrir una terminal desde el menú |
| 3 | Terminal > New Terminal |
| 4 | |
| 5 | 2. Desde la terminal introducir la orden: |
| 6 | Get-ChildItem -path . -Recurse | Select Directory,Name | Out-File archivos.txt |
| 7 | |
| 8 | 3. Abrir el archivo generado, que se llama |
| 9 | archivos.txt |
| 10 | y sobre este, realizar los pasos que siguen: |
| 11 | |
| 12 | 4. Quita del archivo archivos.txt: |
| 13 | * el encabezado, |
| 14 | * todas las carpetas, |
| 15 | * todos los archivos dentro de .vscode como: |
| 16 | * el archivo .vscode/settings.json, |
| 17 | * el archivo .vscode/launch.json, |
| 18 | * el archivo .htaccess, |
| 19 | * el archivo archivos.txt, |
| 20 | * este archivo (instruccionesListadoSw.txt), |
| 21 | * el archivo jsconfig.json, |
| 22 | * el archivo sw.js, |
| 23 | * el archivo de la base de datos, que termina en ".db" y |
| 24 | está en la carpeta php, |
| 25 | * todos los archivos de php y |
| 26 | * las líneas en blanco del final |
| 27 | |
| 28 | 5. Cambia los \ por / desde Visual Studio Code con las siguientes |
| 29 | combinaciones de teclas: |
| 30 | |
| 31 | Ctrl+H En el diálogo que aparece introduce lo siguiente: |
| 32 | Find:\ |
| 33 | Replace:/ |
| 34 | |
| 35 | Clic en el icono Reemplaza todo o Replace All y luego teclea ESC |
| 36 | |
| 37 | 6. Coloca las comillas y coma del final de cada línea desde Visual |
| 38 | Studio Code con las siguientes combinaciones de teclas: |
| 39 | |
| 40 | Ctrl+H En el diálogo que aparece, selecciona el botón |
| 41 | ".*" |
| 42 | e introduce lo siguiente: |
| 43 | Find:\s*$ |
| 44 | Replace:", |
| 45 | |
| 46 | Clic en el icono Reemplaza todo o Replace All y luego teclea ESC |
| 47 | |
| 48 | 7. Marca la carpeta inicial, presiona la combinación de teclas: |
| 49 | |
| 50 | Shift+Ctrl+L |
| 51 | |
| 52 | borra la selección, teclea " y luego ESC |
| 53 | |
| 54 | 8. Cambia las secuencias de espacios por / con las siguientes |
| 55 | combinaciones de teclas: |
| 56 | |
| 57 | Ctrl+H En el diálogo que aparece, selecciona el botón |
| 58 | ".*" |
| 59 | e introduce lo siguiente: |
| 60 | Find:\s+ |
| 61 | Replace:/ |
| 62 | |
| 63 | Clic en el icono Reemplaza todo o Replace All y luego teclea ESC |
| 64 | |
| 65 | 9. Cambia las "/ por " con las siguientes combinaciones de teclas: |
| 66 | |
| 67 | Ctrl+H En el diálogo que aparece, quita la selección del botón |
| 68 | ".*" |
| 69 | e introduce lo siguiente: |
| 70 | Find:"/ |
| 71 | Replace:" |
| 72 | |
| 73 | Clic en el icono Reemplaza todo o Replace All y luego teclea ESC |
| 74 | |
| 75 | 10. Copia el texto al archivo |
| 76 | sw.js |
| 77 | en el contenido del arreglo llamado ARCHIVOS, pero recuerda |
| 78 | mantener el último elemento, que dice: |
| 79 | "/" |
| 1 | "agrega.html", |
| 2 | "index.html", |
| 3 | "modifica.html", |
| 4 | "errors/datosnojson.html", |
| 5 | "errors/eliminadoincorrecto.html", |
| 6 | "errors/errorinterno.html", |
| 7 | "errors/idincorrecto.html", |
| 8 | "errors/modificacionincorrecta.html", |
| 9 | "errors/nombreincorrecto.html", |
| 10 | "errors/resultadonojson.html", |
| 11 | "js/Bd.js", |
| 12 | "js/esperaUnPocoYSincroniza.js", |
| 13 | "js/PASATIEMPO.js", |
| 14 | "js/pasatiempoAgrega.js", |
| 15 | "js/pasatiempoBusca.js", |
| 16 | "js/pasatiempoConsultaNoEliminados.js", |
| 17 | "js/pasatiempoConsultaTodos.js", |
| 18 | "js/pasatiempoElimina.js", |
| 19 | "js/pasatiempoModifica.js", |
| 20 | "js/pasatiemposReemplaza.js", |
| 21 | "js/renderiza.js", |
| 22 | "js/sincroniza.js", |
| 23 | "js/validaPasatiempo.js", |
| 24 | "js/validaPasatiempos.js", |
| 25 | "js/lib/bdConsulta.js", |
| 26 | "js/lib/bdEjecuta.js", |
| 27 | "js/lib/consume.js", |
| 28 | "js/lib/creaIdCliente.js", |
| 29 | "js/lib/enviaJsonRecibeJson.js", |
| 30 | "js/lib/htmlentities.js", |
| 31 | "js/lib/manejaErrores.js", |
| 32 | "js/lib/muestraError.js", |
| 33 | "js/lib/muestraObjeto.js", |
| 34 | "js/lib/ProblemDetailsError.js", |
| 35 | "js/lib/recibeTexto.js", |
| 36 | "js/lib/recibeTextoObligatorio.js", |
| 37 | "js/lib/registraServiceWorker.js", |
| 38 | "js/lib/validaEntidadObligatoria.js", |
| 1 | /* Este archivo debe estar colocado en la carpeta raíz del sitio. |
| 2 | * |
| 3 | * Cualquier cambio en el contenido de este archivo hace que el service |
| 4 | * worker se reinstale. */ |
| 5 | |
| 6 | /** |
| 7 | * Cambia el número de la versión cuando cambia el contenido de los |
| 8 | * archivos. |
| 9 | * |
| 10 | * El número a la izquierda del punto (.), en este caso <q>1</q>, se |
| 11 | * conoce como número mayor y se cambia cuando se realizan |
| 12 | * modificaciones grandes o importantes. |
| 13 | * |
| 14 | * El número a la derecha del punto (.), en este caso <q>00</q>, se |
| 15 | * conoce como número menor y se cambia cuando se realizan |
| 16 | * modificaciones menores. |
| 17 | */ |
| 18 | const VERSION = "1.00" |
| 19 | |
| 20 | /** |
| 21 | * Nombre de la carpeta de caché. |
| 22 | */ |
| 23 | const CACHE = "sincro" |
| 24 | |
| 25 | /** |
| 26 | * Archivos requeridos para que la aplicación funcione fuera de línea. |
| 27 | */ |
| 28 | const ARCHIVOS = [ |
| 29 | "agrega.html", |
| 30 | "index.html", |
| 31 | "modifica.html", |
| 32 | "errors/datosnojson.html", |
| 33 | "errors/eliminadoincorrecto.html", |
| 34 | "errors/errorinterno.html", |
| 35 | "errors/idincorrecto.html", |
| 36 | "errors/modificacionincorrecta.html", |
| 37 | "errors/nombreincorrecto.html", |
| 38 | "errors/resultadonojson.html", |
| 39 | "js/Bd.js", |
| 40 | "js/esperaUnPocoYSincroniza.js", |
| 41 | "js/PASATIEMPO.js", |
| 42 | "js/pasatiempoAgrega.js", |
| 43 | "js/pasatiempoBusca.js", |
| 44 | "js/pasatiempoConsultaNoEliminados.js", |
| 45 | "js/pasatiempoConsultaTodos.js", |
| 46 | "js/pasatiempoElimina.js", |
| 47 | "js/pasatiempoModifica.js", |
| 48 | "js/pasatiemposReemplaza.js", |
| 49 | "js/renderiza.js", |
| 50 | "js/sincroniza.js", |
| 51 | "js/validaPasatiempo.js", |
| 52 | "js/validaPasatiempos.js", |
| 53 | "js/lib/bdConsulta.js", |
| 54 | "js/lib/bdEjecuta.js", |
| 55 | "js/lib/consume.js", |
| 56 | "js/lib/creaIdCliente.js", |
| 57 | "js/lib/enviaJsonRecibeJson.js", |
| 58 | "js/lib/htmlentities.js", |
| 59 | "js/lib/manejaErrores.js", |
| 60 | "js/lib/muestraError.js", |
| 61 | "js/lib/muestraObjeto.js", |
| 62 | "js/lib/ProblemDetailsError.js", |
| 63 | "js/lib/recibeTexto.js", |
| 64 | "js/lib/recibeTextoObligatorio.js", |
| 65 | "js/lib/registraServiceWorker.js", |
| 66 | "js/lib/validaEntidadObligatoria.js", |
| 67 | "/" |
| 68 | ] |
| 69 | |
| 70 | // Verifica si el código corre dentro de un service worker. |
| 71 | if (self instanceof ServiceWorkerGlobalScope) { |
| 72 | // Evento al empezar a instalar el servide worker, |
| 73 | self.addEventListener("install", |
| 74 | (/** @type {ExtendableEvent} */ evt) => { |
| 75 | console.log("El service worker se está instalando.") |
| 76 | evt.waitUntil(llenaElCache()) |
| 77 | }) |
| 78 | |
| 79 | // Evento al solicitar información a la red. |
| 80 | self.addEventListener("fetch", (/** @type {FetchEvent} */ evt) => { |
| 81 | if (evt.request.method === "GET") { |
| 82 | evt.respondWith(buscaLaRespuestaEnElCache(evt)) |
| 83 | } |
| 84 | }) |
| 85 | |
| 86 | // Evento cuando el service worker se vuelve activo. |
| 87 | self.addEventListener("activate", |
| 88 | () => console.log("El service worker está activo.")) |
| 89 | } |
| 90 | |
| 91 | async function llenaElCache() { |
| 92 | console.log("Intentando cargar caché:", CACHE) |
| 93 | // Borra todos los cachés. |
| 94 | const keys = await caches.keys() |
| 95 | for (const key of keys) { |
| 96 | await caches.delete(key) |
| 97 | } |
| 98 | // Abre el caché de este service worker. |
| 99 | const cache = await caches.open(CACHE) |
| 100 | // Carga el listado de ARCHIVOS. |
| 101 | await cache.addAll(ARCHIVOS) |
| 102 | console.log("Cache cargado:", CACHE) |
| 103 | console.log("Versión:", VERSION) |
| 104 | } |
| 105 | |
| 106 | /** @param {FetchEvent} evt */ |
| 107 | async function buscaLaRespuestaEnElCache(evt) { |
| 108 | // Abre el caché. |
| 109 | const cache = await caches.open(CACHE) |
| 110 | const request = evt.request |
| 111 | /* Busca la respuesta a la solicitud en el contenido del caché, sin |
| 112 | * tomar en cuenta la parte después del símbolo "?" en la URL. */ |
| 113 | const response = await cache.match(request, { ignoreSearch: true }) |
| 114 | if (response === undefined) { |
| 115 | /* Si no la encuentra, empieza a descargar de la red y devuelve |
| 116 | * la promesa. */ |
| 117 | return fetch(request) |
| 118 | } else { |
| 119 | // Si la encuentra, devuelve la respuesta encontrada en el caché. |
| 120 | return response |
| 121 | } |
| 122 | } |
| 1 | export const ALMACEN_PASATIEMPO = "PASATIEMPO" |
| 2 | export const PAS_ID = "PAS_ID" |
| 3 | export const INDICE_NOMBRE = "INDICE_NOMBRE" |
| 4 | export const PAS_NOMBRE = "PAS_NOMBRE" |
| 5 | const BD_NOMBRE = "sincro" |
| 6 | const BD_VERSION = 1 |
| 7 | |
| 8 | /** @type { Promise<IDBDatabase> } */ |
| 9 | export const Bd = new Promise((resolve, reject) => { |
| 10 | |
| 11 | /* Se solicita abrir la base de datos, indicando nombre y |
| 12 | * número de versión. */ |
| 13 | const solicitud = indexedDB.open(BD_NOMBRE, BD_VERSION) |
| 14 | |
| 15 | // Si se presenta un error, rechaza la promesa. |
| 16 | solicitud.onerror = () => reject(solicitud.error) |
| 17 | |
| 18 | // Si se abre con éxito, devuelve una conexión a la base de datos. |
| 19 | solicitud.onsuccess = () => resolve(solicitud.result) |
| 20 | |
| 21 | // Si es necesario, se inicia una transacción para cambio de versión. |
| 22 | solicitud.onupgradeneeded = () => { |
| 23 | |
| 24 | const bd = solicitud.result |
| 25 | |
| 26 | // Como hay cambio de versión, borra el almacén si es que existe. |
| 27 | if (bd.objectStoreNames.contains(ALMACEN_PASATIEMPO)) { |
| 28 | bd.deleteObjectStore(ALMACEN_PASATIEMPO) |
| 29 | } |
| 30 | |
| 31 | // Crea el almacén "PASATIEMPO" con el campo llave "PAS_ID". |
| 32 | const almacenPasatiempo = |
| 33 | bd.createObjectStore(ALMACEN_PASATIEMPO, { keyPath: PAS_ID }) |
| 34 | |
| 35 | // Crea un índice ordenado por el campo PAS_NOMBRE que no acepta duplicados. |
| 36 | almacenPasatiempo.createIndex(INDICE_NOMBRE, PAS_NOMBRE) |
| 37 | } |
| 38 | |
| 39 | }) |
| 1 | import { sincroniza } from "./sincroniza.js" |
| 2 | |
| 3 | /** |
| 4 | * Cada 20 segundos (2000 milisegundos) después de la última |
| 5 | * sincronización, los datos se envían al servidor para volver a |
| 6 | * sincronizarse con los datos del servidor. |
| 7 | */ |
| 8 | const MILISEGUNDOS_PARA_VOLVER_A_SINCRONIZAR = 20000 |
| 9 | |
| 10 | export function esperaUnPocoYSincroniza() { |
| 11 | setTimeout(() => sincroniza(), MILISEGUNDOS_PARA_VOLVER_A_SINCRONIZAR) |
| 12 | } |
| 1 | /** |
| 2 | * @typedef {Object} PASATIEMPO |
| 3 | * @property {string} PAS_ID |
| 4 | * @property {string} PAS_NOMBRE |
| 5 | * @property {number} PAS_MODIFICACION |
| 6 | * @property {number} PAS_ELIMINADO |
| 7 | */ |
| 1 | import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js" |
| 2 | import { bdEjecuta } from "./lib/bdEjecuta.js" |
| 3 | import { creaIdCliente } from "./lib/creaIdCliente.js" |
| 4 | import { recibeTextoObligatorio } from "./lib/recibeTextoObligatorio.js" |
| 5 | |
| 6 | |
| 7 | /** |
| 8 | * @param {SubmitEvent} event |
| 9 | */ |
| 10 | export async function pasatiempoAgrega(event) { |
| 11 | |
| 12 | event.preventDefault() |
| 13 | const target = event.target |
| 14 | |
| 15 | if (!(target instanceof HTMLFormElement)) |
| 16 | throw new Error("target no es de tipo form.") |
| 17 | |
| 18 | const formData = new FormData(target) |
| 19 | |
| 20 | const modelo = { |
| 21 | PAS_ID: creaIdCliente(Date.now().toString()), // Genera id único en internet. |
| 22 | PAS_NOMBRE: recibeTextoObligatorio(formData, "nombre"), |
| 23 | PAS_MODIFICACION: Date.now(), |
| 24 | PAS_ELIMINADO: 0, |
| 25 | } |
| 26 | |
| 27 | await bdEjecuta(Bd, [ALMACEN_PASATIEMPO], transaccion => { |
| 28 | const almacenPasatiempo = transaccion.objectStore(ALMACEN_PASATIEMPO) |
| 29 | almacenPasatiempo.add(modelo) |
| 30 | }) |
| 31 | |
| 32 | location.href = "index.html" |
| 33 | |
| 34 | } |
| 1 | import { bdConsulta } from "./lib/bdConsulta.js" |
| 2 | import { validaPasatiempo } from "./validaPasatiempo.js" |
| 3 | import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js" |
| 4 | |
| 5 | /** |
| 6 | * @param {string} id |
| 7 | */ |
| 8 | export async function pasatiempoBusca(id) { |
| 9 | |
| 10 | return bdConsulta(Bd, [ALMACEN_PASATIEMPO], |
| 11 | /** |
| 12 | * @param {(resultado: import("./PASATIEMPO.js").PASATIEMPO|undefined) |
| 13 | * => any} resolve |
| 14 | */ |
| 15 | (transaccion, resolve) => { |
| 16 | |
| 17 | /* Pide el primer objeto de ALMACEN_PASATIEMPO que tenga como llave |
| 18 | * primaria el valor del parámetro id. */ |
| 19 | const consulta = transaccion.objectStore(ALMACEN_PASATIEMPO).get(id) |
| 20 | |
| 21 | // onsuccess se invoca solo una vez, devolviendo el objeto solicitado. |
| 22 | consulta.onsuccess = () => { |
| 23 | /* Se recupera el objeto solicitado usando |
| 24 | * consulta.result |
| 25 | * Si el objeto no se encuentra se recupera undefined. */ |
| 26 | const objeto = consulta.result |
| 27 | if (objeto !== undefined) { |
| 28 | const modelo = validaPasatiempo(objeto) |
| 29 | if (modelo.PAS_ELIMINADO === 0) { |
| 30 | resolve(modelo) |
| 31 | return |
| 32 | } |
| 33 | } |
| 34 | resolve(undefined) |
| 35 | |
| 36 | } |
| 37 | |
| 38 | }) |
| 39 | |
| 40 | } |
| 1 | import { ALMACEN_PASATIEMPO, Bd, INDICE_NOMBRE } from "./Bd.js" |
| 2 | import { bdConsulta } from "./lib/bdConsulta.js" |
| 3 | import { validaPasatiempo } from "./validaPasatiempo.js" |
| 4 | |
| 5 | export async function pasatiempoConsultaNoEliminados() { |
| 6 | |
| 7 | return bdConsulta(Bd, [ALMACEN_PASATIEMPO], |
| 8 | /** |
| 9 | * @param {(resultado: import("./PASATIEMPO.js").PASATIEMPO[])=>void |
| 10 | * } resolve |
| 11 | */ |
| 12 | (transaccion, resolve) => { |
| 13 | |
| 14 | const resultado = [] |
| 15 | |
| 16 | const almacenPasatiempo = transaccion.objectStore(ALMACEN_PASATIEMPO) |
| 17 | |
| 18 | // Usa el índice INDICE_NOMBRE para recuperar los datos ordenados. |
| 19 | const indiceNombre = almacenPasatiempo.index(INDICE_NOMBRE) |
| 20 | |
| 21 | // Pide un cursor para recorrer cada objeto que devuelve la consulta. |
| 22 | const consulta = indiceNombre.openCursor() |
| 23 | |
| 24 | /* onsuccess se invoca por cada uno de los objetos de la consulta y una vez |
| 25 | * cuando se acaban dichos objetos. */ |
| 26 | consulta.onsuccess = () => { |
| 27 | /* El cursor correspondiente al objeto se recupera usando |
| 28 | * consulta.result */ |
| 29 | const cursor = consulta.result |
| 30 | if (cursor === null) { |
| 31 | /* Si el cursor vale null, ya no hay más objetos que procesar; por lo |
| 32 | * mismo, se devuelve el resultado con los pasatiempos recuperados, usando |
| 33 | * resolve(resultado). */ |
| 34 | resolve(resultado) |
| 35 | } else { |
| 36 | /* Si el cursor no vale null y hay más objetos, el siguiente se obtiene con |
| 37 | * cursor.value */ |
| 38 | const modelo = validaPasatiempo(cursor.value) |
| 39 | if (modelo.PAS_ELIMINADO === 0) { |
| 40 | resultado.push(modelo) |
| 41 | } |
| 42 | /* Busca el siguiente objeto de la consulta, que se recupera la siguiente |
| 43 | * vez que se invoque la función onsuccess. */ |
| 44 | cursor.continue() |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | }) |
| 49 | |
| 50 | } |
| 1 | import { bdConsulta } from "./lib/bdConsulta.js" |
| 2 | import { validaPasatiempo } from "./validaPasatiempo.js" |
| 3 | import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js" |
| 4 | |
| 5 | /** |
| 6 | * Lista todos los objetos, incluyendo los que tienen borrado lógico. |
| 7 | */ |
| 8 | export async function pasatiempoConsultaTodos() { |
| 9 | |
| 10 | return bdConsulta(Bd, [ALMACEN_PASATIEMPO], |
| 11 | /** |
| 12 | * @param {(resultado: import("./PASATIEMPO.js").PASATIEMPO[])=>void |
| 13 | * } resolve |
| 14 | */ |
| 15 | (transaccion, resolve) => { |
| 16 | |
| 17 | const resultado = [] |
| 18 | |
| 19 | // Pide un cursor para recorrer cada objeto que devuelve la consulta. |
| 20 | const consulta = transaccion.objectStore(ALMACEN_PASATIEMPO).openCursor() |
| 21 | |
| 22 | /* onsuccess se invoca por cada uno de los objetos de la consulta y una vez |
| 23 | * cuando se acaban dichos objetos. */ |
| 24 | consulta.onsuccess = () => { |
| 25 | /* El cursor correspondiente al objeto se recupera usando |
| 26 | * consulta.result */ |
| 27 | const cursor = consulta.result |
| 28 | if (cursor === null) { |
| 29 | /* Si el cursor vale null, ya no hay más objetos que procesar; por lo |
| 30 | * mismo, se devuelve el resultado con los pasatiempos recuperados, usando |
| 31 | * resolve(resultado). */ |
| 32 | resolve(resultado) |
| 33 | } else { |
| 34 | /* Si el cursor no vale null y hay más objetos, el siguiente se obtiene con |
| 35 | * cursor.value*/ |
| 36 | resultado.push(validaPasatiempo(cursor.value)) |
| 37 | /* Busca el siguiente objeto de la consulta, que se recupera la siguiente |
| 38 | * vez que se invoque la función onsuccess. */ |
| 39 | cursor.continue() |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | }) |
| 44 | |
| 45 | } |
| 1 | import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js" |
| 2 | import { bdEjecuta } from "./lib/bdEjecuta.js" |
| 3 | import { pasatiempoBusca } from "./pasatiempoBusca.js" |
| 4 | |
| 5 | /** |
| 6 | * @param { string } id |
| 7 | */ |
| 8 | export async function pasatiempoElimina(id) { |
| 9 | |
| 10 | if (confirm('Confirma la eliminación')) { |
| 11 | |
| 12 | const modelo = await pasatiempoBusca(id) |
| 13 | |
| 14 | if (modelo !== undefined) { |
| 15 | |
| 16 | modelo.PAS_MODIFICACION = Date.now() |
| 17 | modelo.PAS_ELIMINADO = 1 |
| 18 | await bdEjecuta(Bd, [ALMACEN_PASATIEMPO], transaccion => { |
| 19 | const almacenPasatiempo = transaccion.objectStore(ALMACEN_PASATIEMPO) |
| 20 | almacenPasatiempo.put(modelo) |
| 21 | }) |
| 22 | |
| 23 | } |
| 24 | |
| 25 | location.href = "index.html" |
| 26 | |
| 27 | } |
| 28 | |
| 29 | } |
| 1 | import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js" |
| 2 | import { bdEjecuta } from "./lib/bdEjecuta.js" |
| 3 | import { recibeTextoObligatorio } from "./lib/recibeTextoObligatorio.js" |
| 4 | import { pasatiempoBusca } from "./pasatiempoBusca.js" |
| 5 | |
| 6 | /** |
| 7 | * @param {SubmitEvent} event |
| 8 | * @param {string} id |
| 9 | */ |
| 10 | export async function pasatiempoModifica(event, id) { |
| 11 | |
| 12 | event.preventDefault() |
| 13 | const target = event.target |
| 14 | |
| 15 | if (!(target instanceof HTMLFormElement)) |
| 16 | throw new Error("target no es de tipo form.") |
| 17 | |
| 18 | const formData = new FormData(target) |
| 19 | |
| 20 | const nombre = recibeTextoObligatorio(formData, "nombre") |
| 21 | |
| 22 | const anterior = await pasatiempoBusca(id) |
| 23 | |
| 24 | if (anterior !== undefined) { |
| 25 | |
| 26 | anterior.PAS_NOMBRE = nombre |
| 27 | anterior.PAS_MODIFICACION = Date.now() |
| 28 | |
| 29 | await bdEjecuta(Bd, [ALMACEN_PASATIEMPO], transaccion => { |
| 30 | const almacenPasatiempo = transaccion.objectStore(ALMACEN_PASATIEMPO) |
| 31 | almacenPasatiempo.put(anterior) |
| 32 | }) |
| 33 | |
| 34 | location.href = "index.html" |
| 35 | |
| 36 | } |
| 37 | |
| 38 | } |
| 1 | import { bdEjecuta } from "./lib/bdEjecuta.js" |
| 2 | import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js" |
| 3 | |
| 4 | /** |
| 5 | * Borra el contenido del almacén PASATIEMPO y guarda nuevospasatiempos. |
| 6 | * @param {import("./PASATIEMPO.js").PASATIEMPO[]} nuevosPasatiempos |
| 7 | */ |
| 8 | export async function pasatiemposReemplaza(nuevosPasatiempos) { |
| 9 | return bdEjecuta(Bd, [ALMACEN_PASATIEMPO], transaccion => { |
| 10 | const almacenPasatiempo = transaccion.objectStore(ALMACEN_PASATIEMPO) |
| 11 | almacenPasatiempo.clear() |
| 12 | for (const objeto of nuevosPasatiempos) { |
| 13 | almacenPasatiempo.add(objeto) |
| 14 | } |
| 15 | }) |
| 16 | } |
| 1 | import { htmlentities } from "./lib/htmlentities.js" |
| 2 | import { muestraObjeto } from "./lib/muestraObjeto.js" |
| 3 | |
| 4 | /** |
| 5 | * @param {import("./PASATIEMPO.js").PASATIEMPO[]} pasatiempos |
| 6 | */ |
| 7 | export function renderiza(pasatiempos) { |
| 8 | let render = "" |
| 9 | for (const modelo of pasatiempos) { |
| 10 | const nombre = htmlentities(modelo.PAS_NOMBRE) |
| 11 | const searchParams = new URLSearchParams([["id", modelo.PAS_ID]]) |
| 12 | const params = htmlentities(searchParams.toString()) |
| 13 | render += /* html */ |
| 14 | `<li> |
| 15 | <p><a href="modifica.html?${params}">${nombre}</a></p> |
| 16 | </li>` |
| 17 | } |
| 18 | muestraObjeto( |
| 19 | document, |
| 20 | { |
| 21 | lista: { innerHTML: render } |
| 22 | } |
| 23 | ) |
| 24 | } |
| 25 |
| 1 | import { pasatiempoConsultaTodos } from "./pasatiempoConsultaTodos.js" |
| 2 | import { pasatiemposReemplaza } from "./pasatiemposReemplaza.js" |
| 3 | import { esperaUnPocoYSincroniza } from "./esperaUnPocoYSincroniza.js" |
| 4 | import { consume } from "./lib/consume.js" |
| 5 | import { enviaJsonRecibeJson } from "./lib/enviaJsonRecibeJson.js" |
| 6 | import { muestraError } from "./lib/muestraError.js" |
| 7 | import { renderiza } from "./renderiza.js" |
| 8 | import { validaPasatiempos } from "./validaPasatiempos.js" |
| 9 | |
| 10 | export async function sincroniza() { |
| 11 | |
| 12 | try { |
| 13 | |
| 14 | if (navigator.onLine) { |
| 15 | const todos = await pasatiempoConsultaTodos() |
| 16 | const respuesta = |
| 17 | await consume(enviaJsonRecibeJson("php/sincroniza.php", todos)) |
| 18 | const pasatiempos = validaPasatiempos(await respuesta.json()) |
| 19 | await pasatiemposReemplaza(pasatiempos) |
| 20 | renderiza(pasatiempos) |
| 21 | } |
| 22 | |
| 23 | } catch (error) { |
| 24 | |
| 25 | muestraError(error) |
| 26 | |
| 27 | } |
| 28 | |
| 29 | esperaUnPocoYSincroniza() |
| 30 | |
| 31 | } |
| 1 | /** |
| 2 | * @param { any } objeto |
| 3 | * @returns {import("./PASATIEMPO.js").PASATIEMPO} |
| 4 | */ |
| 5 | export function validaPasatiempo(objeto) { |
| 6 | |
| 7 | if (typeof objeto.PAS_ELIMINADO !== "number" || isNaN(objeto.PAS_ELIMINADO)) |
| 8 | throw new Error("El campo eliminado debe ser número.") |
| 9 | |
| 10 | if (typeof objeto.PAS_ID !== "string" || objeto.PAS_ID === "") |
| 11 | throw new Error("El id debe ser texto que no esté en blanco.") |
| 12 | |
| 13 | if ( |
| 14 | typeof objeto.PAS_MODIFICACION !== "number" || isNaN(objeto.PAS_MODIFICACION) |
| 15 | ) |
| 16 | throw new Error("El campo modificacion debe ser número.") |
| 17 | |
| 18 | if (typeof objeto.PAS_NOMBRE !== "string" || objeto.PAS_ID === "") |
| 19 | throw new Error("El nombre debe ser texto que no esté en blanco.") |
| 20 | |
| 21 | return objeto |
| 22 | |
| 23 | } |
| 1 | import { validaPasatiempo } from "./validaPasatiempo.js" |
| 2 | |
| 3 | /** |
| 4 | * @param { any } objetos |
| 5 | * @returns {import("./PASATIEMPO.js").PASATIEMPO[]} |
| 6 | */ |
| 7 | export function validaPasatiempos(objetos) { |
| 8 | if (!Array.isArray(objetos)) |
| 9 | throw new Error("no se recibió un arreglo.") |
| 10 | /** |
| 11 | * @type {import("./PASATIEMPO.js").PASATIEMPO[]} |
| 12 | */ |
| 13 | const arreglo = [] |
| 14 | for (const objeto of objetos) { |
| 15 | arreglo.push(validaPasatiempo(objeto)) |
| 16 | } |
| 17 | return arreglo |
| 18 | } |
| 1 | /** |
| 2 | * @template T |
| 3 | * @param {Promise<IDBDatabase>} bd |
| 4 | * @param {string[]} almacenes |
| 5 | * @param {(transaccion: IDBTransaction, resolve: (resultado:T)=>void) => any |
| 6 | * } consulta |
| 7 | * @returns {Promise<T>} |
| 8 | */ |
| 9 | export async function bdConsulta(bd, almacenes, consulta) { |
| 10 | |
| 11 | const base = await bd |
| 12 | |
| 13 | return new Promise((resolve, reject) => { |
| 14 | // Inicia una transacción de solo lectura. |
| 15 | const transaccion = base.transaction(almacenes, "readonly") |
| 16 | // Al terminar con error ejecuta la función reject. |
| 17 | transaccion.onerror = () => reject(transaccion.error) |
| 18 | // Estas son las operaciones para realizar la consulta. |
| 19 | consulta(transaccion, resolve) |
| 20 | }) |
| 21 | |
| 22 | } |
| 1 | /** |
| 2 | * @param {Promise<IDBDatabase>} bd |
| 3 | * @param {string[]} entidades |
| 4 | * @param {(t:IDBTransaction) => void} operaciones |
| 5 | */ |
| 6 | export async function bdEjecuta(bd, entidades, operaciones) { |
| 7 | |
| 8 | // Espera que se abra la bd |
| 9 | const base = await bd |
| 10 | |
| 11 | return new Promise( |
| 12 | (resolve, reject) => { |
| 13 | // Inicia una transacción de lectura y escritura. |
| 14 | const transaccion = base.transaction(entidades, "readwrite") |
| 15 | // Al terminar con éxito, ejecuta la función resolve. |
| 16 | transaccion.oncomplete = resolve |
| 17 | // Al terminar con error, ejecuta la función reject. |
| 18 | transaccion.onerror = () => reject(transaccion.error) |
| 19 | // Estas son las operaciones de la transacción. |
| 20 | operaciones(transaccion) |
| 21 | }) |
| 22 | |
| 23 | } |
| 1 | import { ProblemDetailsError } from "./ProblemDetailsError.js" |
| 2 | |
| 3 | /** |
| 4 | * Espera a que la promesa de un fetch termine. Si |
| 5 | * hay error, lanza una excepción. |
| 6 | * |
| 7 | * @param {Promise<Response> } servicio |
| 8 | */ |
| 9 | export async function consume(servicio) { |
| 10 | const respuesta = await servicio |
| 11 | if (respuesta.ok) { |
| 12 | return respuesta |
| 13 | } else { |
| 14 | const contentType = respuesta.headers.get("Content-Type") |
| 15 | if ( |
| 16 | contentType !== null && contentType.startsWith("application/problem+json") |
| 17 | ) |
| 18 | throw new ProblemDetailsError(await respuesta.json()) |
| 19 | else |
| 20 | throw new Error(respuesta.statusText) |
| 21 | } |
| 22 | } |
| 1 | /** |
| 2 | * Añade caracteres al azar a una raíz, para obtener un clientId único. |
| 3 | * @param {string} raiz |
| 4 | */ |
| 5 | export function creaIdCliente(raiz) { |
| 6 | const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" |
| 7 | for (var i = 0; i < 15; i++) { |
| 8 | raiz += chars.charAt(Math.floor(Math.random() * chars.length)) |
| 9 | } |
| 10 | return raiz |
| 11 | } |
| 1 | |
| 2 | /** |
| 3 | * @param { string } url |
| 4 | * @param { Object } body |
| 5 | * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS" |
| 6 | * | "CONNECT" | "HEAD" } metodoHttp |
| 7 | */ |
| 8 | export async function enviaJsonRecibeJson(url, body, metodoHttp = "POST") { |
| 9 | return fetch( |
| 10 | url, |
| 11 | { |
| 12 | method: metodoHttp, |
| 13 | headers: { |
| 14 | "Content-Type": "application/json", |
| 15 | "Accept": "application/json, application/problem+json" |
| 16 | }, |
| 17 | body: JSON.stringify(body) |
| 18 | } |
| 19 | ) |
| 20 | } |
| 1 | /** |
| 2 | * Codifica un texto para que cambie los caracteres |
| 3 | * especiales y no se pueda interpretar como |
| 4 | * etiiqueta HTML. Esta técnica evita la inyección |
| 5 | * de código. |
| 6 | * @param { string } texto |
| 7 | */ |
| 8 | export function htmlentities(texto) { |
| 9 | return texto.replace(/[<>"']/g, textoDetectado => { |
| 10 | switch (textoDetectado) { |
| 11 | case "<": return "<" |
| 12 | case ">": return ">" |
| 13 | case '"': return """ |
| 14 | case "'": return "'" |
| 15 | default: return textoDetectado |
| 16 | } |
| 17 | }) |
| 18 | } |
| 19 |
| 1 | import { muestraError } from "./muestraError.js" |
| 2 | |
| 3 | /** |
| 4 | * Intercepta Response.prototype.json para capturar errores de parseo |
| 5 | * y asegurar que se reporten correctamente en navegadores Chromium. |
| 6 | */ |
| 7 | { |
| 8 | const originalJson = Response.prototype.json |
| 9 | |
| 10 | Response.prototype.json = function () { |
| 11 | // Llamamos al método original usando el contexto (this) de la respuesta |
| 12 | return originalJson.apply(this, arguments) |
| 13 | .catch((/** @type {any} */ error) => { |
| 14 | // Corrige un error de Chrome que evita el manejo correcto de errores. |
| 15 | throw new Error(error) |
| 16 | }) |
| 17 | } |
| 18 | } |
| 19 | |
| 20 | window.onerror = function ( |
| 21 | /** @type {string} */ _message, |
| 22 | /** @type {string} */ _url, |
| 23 | /** @type {number} */ _line, |
| 24 | /** @type {number} */ _column, |
| 25 | /** @type {Error} */ errorObject |
| 26 | ) { |
| 27 | muestraError(errorObject) |
| 28 | return true |
| 29 | } |
| 30 | |
| 31 | window.addEventListener('unhandledrejection', event => { |
| 32 | muestraError(event.reason) |
| 33 | event.preventDefault() |
| 34 | }) |
| 35 |
| 1 | import { ProblemDetailsError } from "./ProblemDetailsError.js" |
| 2 | |
| 3 | /** |
| 4 | * Muestra los datos de una Error en la consola y en un cuadro de alerta. |
| 5 | * @param { ProblemDetailsError | Error | null } error descripción del error. |
| 6 | */ |
| 7 | export function muestraError(error) { |
| 8 | |
| 9 | if (error === null) { |
| 10 | |
| 11 | console.error("Error") |
| 12 | alert("Error") |
| 13 | |
| 14 | } else if (error instanceof ProblemDetailsError) { |
| 15 | |
| 16 | const problemDetails = error.problemDetails |
| 17 | |
| 18 | let mensaje = |
| 19 | typeof problemDetails["title"] === "string" ? problemDetails["title"] : "" |
| 20 | if (typeof problemDetails["detail"] === "string") { |
| 21 | if (mensaje !== "") { |
| 22 | mensaje += "\n\n" |
| 23 | } |
| 24 | mensaje += problemDetails["detail"] |
| 25 | } |
| 26 | if (mensaje === "") { |
| 27 | mensaje = "Error" |
| 28 | } |
| 29 | console.error(error, problemDetails) |
| 30 | alert(mensaje) |
| 31 | |
| 32 | } else { |
| 33 | |
| 34 | console.error(error) |
| 35 | alert(error.message) |
| 36 | |
| 37 | } |
| 38 | |
| 39 | } |
| 1 | /** |
| 2 | * @param {Document | HTMLElement | ShadowRoot} raizHtml |
| 3 | * @param { any } objeto |
| 4 | */ |
| 5 | export function muestraObjeto(raizHtml, objeto) { |
| 6 | for (const [nombre, definiciones] of Object.entries(objeto)) { |
| 7 | if (Array.isArray(definiciones)) { |
| 8 | muestraArray(raizHtml, nombre, definiciones) |
| 9 | } else if (definiciones !== undefined && definiciones !== null) { |
| 10 | muestraElemento(raizHtml, nombre, definiciones) |
| 11 | } |
| 12 | } |
| 13 | } |
| 14 | |
| 15 | /** |
| 16 | * @param { string } nombre |
| 17 | */ |
| 18 | export function selectorDeNombre(nombre) { |
| 19 | return `[id="${nombre}"],[name="${nombre}"],[data-name="${nombre}"]` |
| 20 | } |
| 21 | |
| 22 | /** |
| 23 | * @param { Document | HTMLElement | ShadowRoot } raizHtml |
| 24 | * @param { string } propiedad |
| 25 | * @param {any[]} valores |
| 26 | */ |
| 27 | function muestraArray(raizHtml, propiedad, valores) { |
| 28 | const conjunto = new Set(valores) |
| 29 | const elementos = raizHtml.querySelectorAll(selectorDeNombre(propiedad)) |
| 30 | if (elementos.length === 1 && elementos[0] instanceof HTMLSelectElement) { |
| 31 | muestraOptions(elementos[0], conjunto) |
| 32 | } else { |
| 33 | muestraInputs(elementos, conjunto) |
| 34 | } |
| 35 | |
| 36 | } |
| 37 | |
| 38 | /** |
| 39 | * @param {HTMLSelectElement} select |
| 40 | * @param {Set<any>} conjunto |
| 41 | */ |
| 42 | function muestraOptions(select, conjunto) { |
| 43 | for (let i = 0, options = select.options, len = options.length; i < len; i++) { |
| 44 | const option = options[i] |
| 45 | option.selected = conjunto.has(option.value) |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | /** |
| 50 | * @param {NodeListOf<Element>} elementos |
| 51 | * @param {Set<any>} conjunto |
| 52 | */ |
| 53 | function muestraInputs(elementos, conjunto) { |
| 54 | for (let i = 0, len = elementos.length; i < len; i++) { |
| 55 | const elemento = elementos[i] |
| 56 | if (elemento instanceof HTMLInputElement) { |
| 57 | elemento.checked = conjunto.has(elemento.value) |
| 58 | } |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | const data_ = "data-" |
| 63 | const data_Length = data_.length |
| 64 | |
| 65 | /** |
| 66 | * @param {Document | HTMLElement | ShadowRoot} raizHtml |
| 67 | * @param {string} nombre |
| 68 | * @param {{ [s: string]: any; } } definiciones |
| 69 | */ |
| 70 | function muestraElemento(raizHtml, nombre, definiciones) { |
| 71 | const elemento = raizHtml.querySelector(selectorDeNombre(nombre)) |
| 72 | if (elemento !== null) { |
| 73 | for (const [propiedad, valor] of Object.entries(definiciones)) { |
| 74 | if (propiedad in elemento) { |
| 75 | elemento[propiedad] = valor |
| 76 | } else if ( |
| 77 | propiedad.length > data_Length |
| 78 | && propiedad.startsWith(data_) |
| 79 | && elemento instanceof HTMLElement |
| 80 | ) { |
| 81 | elemento.dataset[propiedad.substring(data_Length)] = valor |
| 82 | } |
| 83 | } |
| 84 | } |
| 85 | } |
| 1 | export class ProblemDetailsError extends Error { |
| 2 | |
| 3 | /** |
| 4 | * Detalle de los errores devueltos por un servicio. |
| 5 | * Crea una instancia de ProblemDetailsError. |
| 6 | * @param {object} problemDetails Objeto con la descripcipon del error. |
| 7 | */ |
| 8 | constructor(problemDetails) { |
| 9 | |
| 10 | super(typeof problemDetails["detail"] === "string" |
| 11 | ? problemDetails["detail"] |
| 12 | : (typeof problemDetails["title"] === "string" |
| 13 | ? problemDetails["title"] |
| 14 | : "Error")) |
| 15 | |
| 16 | this.problemDetails = problemDetails |
| 17 | |
| 18 | } |
| 19 | |
| 20 | } |
| 1 | /** |
| 2 | * @param {FormData} formData |
| 3 | * @param {string} parametro |
| 4 | */ |
| 5 | export function recibeTexto(formData, parametro) { |
| 6 | const valor = formData.get(parametro) |
| 7 | if (valor !== null && typeof valor !== "string") |
| 8 | throw new Error(`El valor de ${parametro} debe ser texto.`) |
| 9 | return valor === null ? undefined : valor |
| 10 | } |
| 11 |
| 1 | import { recibeTexto } from "./recibeTexto.js" |
| 2 | |
| 3 | /** |
| 4 | * @param {FormData} formData |
| 5 | * @param {string} parametro |
| 6 | */ |
| 7 | export function recibeTextoObligatorio(formData, parametro) { |
| 8 | const texto = recibeTexto(formData, parametro) |
| 9 | if (texto === undefined) throw new Error(`Falta el valor de ${parametro}.`) |
| 10 | const trimTexto = texto.trim() |
| 11 | if (trimTexto === "") throw new Error(`Campo ${parametro} en blanco.`) |
| 12 | return trimTexto |
| 13 | } |
| 14 |
| 1 | const nombreDeServiceWorker = "sw.js" |
| 2 | |
| 3 | try { |
| 4 | navigator.serviceWorker.register(nombreDeServiceWorker) |
| 5 | .then(registro => { |
| 6 | console.log(nombreDeServiceWorker, "registrado.") |
| 7 | console.log(registro) |
| 8 | }) |
| 9 | .catch(error => console.log(error)) |
| 10 | } catch (error) { |
| 11 | console.log(error) |
| 12 | } |
| 1 | /** |
| 2 | * @template T |
| 3 | * @param {string} nombre |
| 4 | * @param {T | undefined} entidad |
| 5 | */ |
| 6 | export function validaEntidadObligatoria(nombre, entidad) { |
| 7 | |
| 8 | if (entidad === undefined) |
| 9 | throw new Error(`Registro de ${nombre} no encontrado.`) |
| 10 | |
| 11 | return entidad |
| 12 | } |
| 13 |
| 1 | <?php |
| 2 | |
| 3 | class Bd |
| 4 | { |
| 5 | |
| 6 | private static ?PDO $pdo = null; |
| 7 | |
| 8 | static function pdo(): PDO |
| 9 | { |
| 10 | if (self::$pdo === null) { |
| 11 | self::$pdo = new PDO( |
| 12 | // cadena de conexión |
| 13 | "sqlite:" . __DIR__ . "/sincro.db", |
| 14 | // usuario |
| 15 | null, |
| 16 | // contraseña |
| 17 | null, |
| 18 | // Opciones: pdos no persistentes y lanza excepciones. |
| 19 | [PDO::ATTR_PERSISTENT => false, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] |
| 20 | ); |
| 21 | |
| 22 | self::$pdo->exec( |
| 23 | 'CREATE TABLE IF NOT EXISTS PASATIEMPO ( |
| 24 | PAS_ID TEXT NOT NULL, |
| 25 | PAS_NOMBRE TEXT NOT NULL, |
| 26 | PAS_MODIFICACION INTEGER NOT NULL, |
| 27 | PAS_ELIMINADO INTEGER NOT NULL, |
| 28 | CONSTRAINT PAS_PK |
| 29 | PRIMARY KEY(PAS_ID), |
| 30 | CONSTRAINT PAS_ID_NV |
| 31 | CHECK(LENGTH(PAS_ID) > 0), |
| 32 | CONSTRAINT PAS_NOM_NV |
| 33 | CHECK(LENGTH(PAS_NOMBRE) > 0) |
| 34 | )' |
| 35 | ); |
| 36 | } |
| 37 | |
| 38 | return self::$pdo; |
| 39 | } |
| 40 | } |
| 41 |
| 1 | <?php |
| 2 | |
| 3 | require_once __DIR__ . "/Bd.php"; |
| 4 | require_once __DIR__ . "/TABLA_PASATIEMPO.php"; |
| 5 | |
| 6 | /** |
| 7 | * @param array{ |
| 8 | * PAS_ID: string, |
| 9 | * PAS_NOMBRE: string, |
| 10 | * PAS_MODIFICACION: int, |
| 11 | * PAS_ELIMINADO: int |
| 12 | * } $modelo |
| 13 | */ |
| 14 | function pasatiempoAgrega(array $modelo) |
| 15 | { |
| 16 | $bd = Bd::pdo(); |
| 17 | $stmt = $bd->prepare( |
| 18 | "INSERT INTO PASATIEMPO ( |
| 19 | PAS_ID, PAS_NOMBRE, PAS_MODIFICACION, PAS_ELIMINADO |
| 20 | ) values ( |
| 21 | :PAS_ID, :PAS_NOMBRE, :PAS_MODIFICACION, :PAS_ELIMINADO |
| 22 | )" |
| 23 | ); |
| 24 | $stmt->execute([ |
| 25 | ":PAS_ID" => $modelo[PAS_ID], |
| 26 | ":PAS_NOMBRE" => $modelo[PAS_NOMBRE], |
| 27 | ":PAS_MODIFICACION" => $modelo[PAS_MODIFICACION], |
| 28 | ":PAS_ELIMINADO" => $modelo[PAS_ELIMINADO], |
| 29 | ]); |
| 30 | } |
| 31 |
| 1 | <?php |
| 2 | |
| 3 | require_once __DIR__ . "/Bd.php"; |
| 4 | require_once __DIR__ . "/TABLA_PASATIEMPO.php"; |
| 5 | |
| 6 | /** |
| 7 | * @return false | array{ |
| 8 | * PAS_ID: string, |
| 9 | * PAS_NOMBRE: string, |
| 10 | * PAS_MODIFICACION: int, |
| 11 | * PAS_ELIMINADO: int |
| 12 | * } |
| 13 | */ |
| 14 | function pasatiempoBusca(string $id): false|array |
| 15 | { |
| 16 | $bd = Bd::pdo(); |
| 17 | $stmt = $bd->prepare("SELECT * FROM PASATIEMPO WHERE PAS_ID = :PAS_ID"); |
| 18 | $stmt->execute([":PAS_ID" => $id]); |
| 19 | $modelo = $stmt->fetch(PDO::FETCH_ASSOC); |
| 20 | return $modelo; |
| 21 | } |
| 22 |
| 1 | <?php |
| 2 | |
| 3 | require_once __DIR__ . "/Bd.php"; |
| 4 | require_once __DIR__ . "/TABLA_PASATIEMPO.php"; |
| 5 | |
| 6 | /** |
| 7 | * @return array{ |
| 8 | * PAS_ID: string, |
| 9 | * PAS_NOMBRE: string, |
| 10 | * PAS_MODIFICACION: int, |
| 11 | * PAS_ELIMINADO: int |
| 12 | * }[] |
| 13 | */ |
| 14 | function pasatiempoConsultaNoEliminados() |
| 15 | { |
| 16 | $bd = Bd::pdo(); |
| 17 | $stmt = $bd->query( |
| 18 | "SELECT * FROM PASATIEMPO WHERE PAS_ELIMINADO = 0 ORDER BY PAS_NOMBRE" |
| 19 | ); |
| 20 | $lista = $stmt->fetchAll(PDO::FETCH_ASSOC); |
| 21 | return $lista; |
| 22 | } |
| 23 |
| 1 | <?php |
| 2 | |
| 3 | require_once __DIR__ . "/Bd.php"; |
| 4 | require_once __DIR__ . "/TABLA_PASATIEMPO.php"; |
| 5 | |
| 6 | /** |
| 7 | * @param array{ |
| 8 | * PAS_ID: string, |
| 9 | * PAS_NOMBRE: string, |
| 10 | * PAS_MODIFICACION: int, |
| 11 | * PAS_ELIMINADO: int |
| 12 | * } $modelo |
| 13 | */ |
| 14 | function pasatiempoModifica(array $modelo) |
| 15 | { |
| 16 | $bd = Bd::pdo(); |
| 17 | $stmt = $bd->prepare( |
| 18 | "UPDATE PASATIEMPO |
| 19 | SET |
| 20 | PAS_NOMBRE = :PAS_NOMBRE, |
| 21 | PAS_MODIFICACION = :PAS_MODIFICACION, |
| 22 | PAS_ELIMINADO = :PAS_ELIMINADO |
| 23 | WHERE |
| 24 | PAS_ID = :PAS_ID" |
| 25 | ); |
| 26 | $stmt->execute([ |
| 27 | ":PAS_ID" => $modelo[PAS_ID], |
| 28 | ":PAS_NOMBRE" => $modelo[PAS_NOMBRE], |
| 29 | ":PAS_MODIFICACION" => $modelo[PAS_MODIFICACION], |
| 30 | ":PAS_ELIMINADO" => $modelo[PAS_ELIMINADO], |
| 31 | ]); |
| 32 | } |
| 33 |
| 1 | <?php |
| 2 | |
| 3 | require_once __DIR__ . "/lib/manejaErrores.php"; |
| 4 | require_once __DIR__ . "/lib/recibeJson.php"; |
| 5 | require_once __DIR__ . "/lib/devuelveJson.php"; |
| 6 | require_once __DIR__ . "/TABLA_PASATIEMPO.php"; |
| 7 | require_once __DIR__ . "/validaPasatiempo.php"; |
| 8 | require_once __DIR__ . "/pasatiempoAgrega.php"; |
| 9 | require_once __DIR__ . "/pasatiempoBusca.php"; |
| 10 | require_once __DIR__ . "/pasatiempoConsultaNoEliminados.php"; |
| 11 | require_once __DIR__ . "/pasatiempoModifica.php"; |
| 12 | |
| 13 | $lista = recibeJson(); |
| 14 | |
| 15 | if (!is_array($lista)) { |
| 16 | $lista = []; |
| 17 | } |
| 18 | |
| 19 | foreach ($lista as $modelo) { |
| 20 | $modeloEnElCliente = validaPasatiempo($modelo); |
| 21 | $modeloEnElServidor = pasatiempoBusca($modeloEnElCliente[PAS_ID]); |
| 22 | |
| 23 | if ($modeloEnElServidor === false) { |
| 24 | |
| 25 | /* CONFLICTO: El modelo no ha estado en el servidor. |
| 26 | * AGREGARLO solamente si no está eliminado. */ |
| 27 | if ($modeloEnElCliente[PAS_ELIMINADO] === 0) { |
| 28 | pasatiempoAgrega($modeloEnElCliente); |
| 29 | } |
| 30 | } elseif ( |
| 31 | $modeloEnElServidor[PAS_ELIMINADO] === 0 |
| 32 | && $modeloEnElCliente[PAS_ELIMINADO] === 1 |
| 33 | ) { |
| 34 | |
| 35 | /* CONFLICTO: El registro está en el servidor, donde no se ha eliminado, pero |
| 36 | * ha sido eliminado en el cliente. |
| 37 | * Gana el cliente, porque optamos por no revivir lo eliminado. */ |
| 38 | pasatiempoModifica($modeloEnElCliente); |
| 39 | } else if ( |
| 40 | $modeloEnElCliente[PAS_ELIMINADO] === 0 |
| 41 | && $modeloEnElServidor[PAS_ELIMINADO] === 0 |
| 42 | ) { |
| 43 | |
| 44 | /* CONFLICTO: Registros en el servidor y en el cliente. Pueden ser |
| 45 | * diferentes. |
| 46 | * GANA FECHA MÁS GRANDE. Cuando gana el servidor, no se hace nada. */ |
| 47 | if ( |
| 48 | $modeloEnElCliente[PAS_MODIFICACION] > |
| 49 | $modeloEnElServidor[PAS_MODIFICACION] |
| 50 | ) { |
| 51 | // La versión del cliente es más nueva y prevalece. |
| 52 | pasatiempoModifica($modeloEnElCliente); |
| 53 | } |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | $lista = pasatiempoConsultaNoEliminados(); |
| 58 | |
| 59 | devuelveJson($lista); |
| 60 |
| 1 | <?php |
| 2 | |
| 3 | const PASATIEMPO = "PASATIEMPO"; |
| 4 | const PAS_ID = "PAS_ID"; |
| 5 | const PAS_NOMBRE = "PAS_NOMBRE"; |
| 6 | const PAS_MODIFICACION = "PAS_MODIFICACION"; |
| 7 | const PAS_ELIMINADO = "PAS_ELIMINADO"; |
| 8 |
| 1 | <?php |
| 2 | |
| 3 | require_once __DIR__ . "/lib/BAD_REQUEST.php"; |
| 4 | require_once __DIR__ . "/lib/ProblemDetailsException.php"; |
| 5 | require_once __DIR__ . "/TABLA_PASATIEMPO.php"; |
| 6 | |
| 7 | function validaPasatiempo($objeto) |
| 8 | { |
| 9 | if (!isset($objeto->PAS_ELIMINADO) || !is_int($objeto->PAS_ELIMINADO)) |
| 10 | throw new ProblemDetailsException([ |
| 11 | "status" => BAD_REQUEST, |
| 12 | "title" => "El campo eliminado debe ser entero.", |
| 13 | "type" => "/errors/eliminadoincorrecto.html", |
| 14 | ]); |
| 15 | |
| 16 | if ( |
| 17 | !isset($objeto->PAS_ID) |
| 18 | || !is_string($objeto->PAS_ID) |
| 19 | || $objeto->PAS_ID === "" |
| 20 | ) |
| 21 | throw new ProblemDetailsException([ |
| 22 | "status" => BAD_REQUEST, |
| 23 | "title" => "El id debe ser texto que no esté en blanco.", |
| 24 | "type" => "/errors/idincorrecto.html", |
| 25 | ]); |
| 26 | |
| 27 | if (!isset($objeto->PAS_MODIFICACION) || !is_int($objeto->PAS_MODIFICACION)) |
| 28 | throw new ProblemDetailsException([ |
| 29 | "status" => BAD_REQUEST, |
| 30 | "title" => "La modificacion debe ser número.", |
| 31 | "type" => "/errors/modificacionincorrecta.html", |
| 32 | ]); |
| 33 | |
| 34 | if ( |
| 35 | !isset($objeto->PAS_NOMBRE) |
| 36 | || !is_string($objeto->PAS_NOMBRE) |
| 37 | || $objeto->PAS_NOMBRE === "" |
| 38 | ) |
| 39 | throw new ProblemDetailsException([ |
| 40 | "status" => BAD_REQUEST, |
| 41 | "title" => "El nombre debe ser texto que no esté en blanco.", |
| 42 | "type" => "/errors/nombreincorrecto.html", |
| 43 | ]); |
| 44 | |
| 45 | return [ |
| 46 | PAS_ELIMINADO => $objeto->PAS_ELIMINADO, |
| 47 | PAS_ID => $objeto->PAS_ID, |
| 48 | PAS_NOMBRE => $objeto->PAS_NOMBRE, |
| 49 | PAS_MODIFICACION => $objeto->PAS_MODIFICACION, |
| 50 | ]; |
| 51 | } |
| 52 |
| 1 | <?php |
| 2 | |
| 3 | const BAD_REQUEST = 400; |
| 4 |
| 1 | <?php |
| 2 | |
| 3 | require_once __DIR__ . "/devuelveResultadoNoJson.php"; |
| 4 | |
| 5 | function devuelveJson($resultado) |
| 6 | { |
| 7 | $json = json_encode($resultado); |
| 8 | if ($json === false) { |
| 9 | devuelveResultadoNoJson(); |
| 10 | } else { |
| 11 | header("Content-Type: application/json; charset=utf-8"); |
| 12 | echo $json; |
| 13 | } |
| 14 | exit(); |
| 15 | } |
| 16 |
| 1 | <?php |
| 2 | |
| 3 | require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php"; |
| 4 | |
| 5 | function devuelveResultadoNoJson() |
| 6 | { |
| 7 | http_response_code(INTERNAL_SERVER_ERROR); |
| 8 | header("Content-Type: application/problem+json; charset=utf-8"); |
| 9 | |
| 10 | echo '{' . |
| 11 | "status: " . INTERNAL_SERVER_ERROR . |
| 12 | '"title": "El resultado no puede representarse como JSON."' . |
| 13 | '"type": "/errors/resultadonojson.html"' . |
| 14 | '}'; |
| 15 | } |
| 16 |
| 1 | <?php |
| 2 | |
| 3 | const INTERNAL_SERVER_ERROR = 500; |
| 1 | <?php |
| 2 | |
| 3 | require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php"; |
| 4 | require_once __DIR__ . "/ProblemDetailsException.php"; |
| 5 | |
| 6 | // Hace que se lance una excepción automáticamente cuando se genere un error. |
| 7 | set_error_handler(function ($severity, $message, $file, $line) { |
| 8 | throw new ErrorException($message, 0, $severity, $file, $line); |
| 9 | }); |
| 10 | |
| 11 | // Código cuando una excepción no es atrapada. |
| 12 | set_exception_handler(function (Throwable $excepcion) { |
| 13 | if ($excepcion instanceof ProblemDetailsException) { |
| 14 | devuelveProblemDetails($excepcion->problemDetails); |
| 15 | } else { |
| 16 | devuelveProblemDetails([ |
| 17 | "status" => INTERNAL_SERVER_ERROR, |
| 18 | "title" => "Error interno del servidor", |
| 19 | "detail" => $excepcion->getMessage(), |
| 20 | "type" => "/errors/errorinterno.html", |
| 21 | ]); |
| 22 | } |
| 23 | exit(); |
| 24 | }); |
| 25 | |
| 26 | function devuelveProblemDetails(array $array) |
| 27 | { |
| 28 | $json = json_encode($array); |
| 29 | if ($json === false) { |
| 30 | devuelveResultadoNoJson(); |
| 31 | } else { |
| 32 | http_response_code(isset($array["status"]) ? $array["status"] : 500); |
| 33 | header("Content-Type: application/problem+json; charset=utf-8"); |
| 34 | echo $json; |
| 35 | } |
| 36 | } |
| 37 |
| 1 | <?php |
| 2 | |
| 3 | require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php"; |
| 4 | |
| 5 | /** |
| 6 | * Detalle de los errores devueltos por un servicio. |
| 7 | */ |
| 8 | class ProblemDetailsException extends Exception |
| 9 | { |
| 10 | |
| 11 | public array $problemDetails; |
| 12 | |
| 13 | public function __construct( |
| 14 | array $problemDetails, |
| 15 | ) { |
| 16 | |
| 17 | parent::__construct( |
| 18 | isset($problemDetails["detail"]) |
| 19 | ? $problemDetails["detail"] |
| 20 | : (isset($problemDetails["title"]) |
| 21 | ? $problemDetails["title"] |
| 22 | : "Error"), |
| 23 | $problemDetails["status"] |
| 24 | ? $problemDetails["status"] |
| 25 | : INTERNAL_SERVER_ERROR |
| 26 | ); |
| 27 | |
| 28 | $this->problemDetails = $problemDetails; |
| 29 | } |
| 30 | } |
| 31 |
| 1 | <?php |
| 2 | |
| 3 | require_once __DIR__ . "/BAD_REQUEST.php"; |
| 4 | |
| 5 | function recibeJson() |
| 6 | { |
| 7 | $json = json_decode(file_get_contents("php://input")); |
| 8 | |
| 9 | if ($json === null) { |
| 10 | |
| 11 | http_response_code(BAD_REQUEST); |
| 12 | header("Content-Type: application/problem+json; charset=utf-8"); |
| 13 | |
| 14 | echo '{' . |
| 15 | "status: " . BAD_REQUEST . |
| 16 | '"title": "Los datos recibidos no están en formato JSON."' . |
| 17 | '"type": "/errors/datosnojson.html"' . |
| 18 | '}'; |
| 19 | |
| 20 | exit(); |
| 21 | } |
| 22 | |
| 23 | return $json; |
| 24 | } |
| 25 |
| 1 | <!DOCTYPE html> |
| 2 | <html lang="es"> |
| 3 | |
| 4 | <head> |
| 5 | |
| 6 | <meta charset="UTF-8"> |
| 7 | <meta name="viewport" content="width=device-width"> |
| 8 | |
| 9 | <title>Los datos recibidos no están en formato JSON</title> |
| 10 | |
| 11 | </head> |
| 12 | |
| 13 | <body> |
| 14 | |
| 15 | <h1>Los datos recibidos no están en formato JSON</h1> |
| 16 | |
| 17 | </body> |
| 18 | |
| 19 | </html> |
| 1 | <!DOCTYPE html> |
| 2 | <html lang="es"> |
| 3 | |
| 4 | <head> |
| 5 | |
| 6 | <meta charset="UTF-8"> |
| 7 | <meta name="viewport" content="width=device-width"> |
| 8 | |
| 9 | <title>El campo eliminado debe ser entero</title> |
| 10 | |
| 11 | <body> |
| 12 | |
| 13 | <h1>El campo eliminado debe ser entero</h1> |
| 14 | |
| 15 | </body> |
| 16 | |
| 17 | </html> |
| 1 | <!DOCTYPE html> |
| 2 | <html lang="es"> |
| 3 | |
| 4 | <head> |
| 5 | |
| 6 | <meta charset="UTF-8"> |
| 7 | <meta name="viewport" content="width=device-width"> |
| 8 | |
| 9 | <title>Error interno del servidor</title> |
| 10 | |
| 11 | </head> |
| 12 | |
| 13 | <body> |
| 14 | |
| 15 | <h1>Error interno del servidor</h1> |
| 16 | |
| 17 | <p>Se presentó de forma inesperada un error interno del servidor.</p> |
| 18 | |
| 19 | </body> |
| 20 | |
| 21 | </html> |
| 1 | <!DOCTYPE html> |
| 2 | <html lang="es"> |
| 3 | |
| 4 | <head> |
| 5 | |
| 6 | <meta charset="UTF-8"> |
| 7 | <meta name="viewport" content="width=device-width"> |
| 8 | |
| 9 | <title>El id debe ser texto que no esté en blanco</title> |
| 10 | |
| 11 | <body> |
| 12 | |
| 13 | <h1>El id debe ser texto que no esté en blanco</h1> |
| 14 | |
| 15 | </body> |
| 16 | |
| 17 | </html> |
| 1 | <!DOCTYPE html> |
| 2 | <html lang="es"> |
| 3 | |
| 4 | <head> |
| 5 | |
| 6 | <meta charset="UTF-8"> |
| 7 | <meta name="viewport" content="width=device-width"> |
| 8 | |
| 9 | <title>La modificacion debe ser número</title> |
| 10 | |
| 11 | <body> |
| 12 | |
| 13 | <h1>La modificacion debe ser número</h1> |
| 14 | |
| 15 | </body> |
| 16 | |
| 17 | </html> |
| 1 | <!DOCTYPE html> |
| 2 | <html lang="es"> |
| 3 | |
| 4 | <head> |
| 5 | |
| 6 | <meta charset="UTF-8"> |
| 7 | <meta name="viewport" content="width=device-width"> |
| 8 | |
| 9 | <title>El nombre debe ser texto que no esté en blanco</title> |
| 10 | |
| 11 | <body> |
| 12 | |
| 13 | <h1>El nombre debe ser texto que no esté en blanco</h1> |
| 14 | |
| 15 | </body> |
| 16 | |
| 17 | </html> |
| 1 | <!DOCTYPE html> |
| 2 | <html lang="es"> |
| 3 | |
| 4 | <head> |
| 5 | |
| 6 | <meta charset="UTF-8"> |
| 7 | <meta name="viewport" content="width=device-width"> |
| 8 | |
| 9 | <title>El resultado no puede representarse como JSON</title> |
| 10 | |
| 11 | </head> |
| 12 | |
| 13 | <body> |
| 14 | |
| 15 | <h1>El resultado no puede representarse como JSON</h1> |
| 16 | |
| 17 | <p> |
| 18 | Debido a un error interno del servidor, el resultado generado, no se puede |
| 19 | recuperar. |
| 20 | </p> |
| 21 | |
| 22 | </body> |
| 23 | |
| 24 | </html> |
Este archivo configura las respuestas del servidor..
Lo utilizan principalmente servidores como Apache o Nginx.
Configura el mime type para el archivo de manifiesto y sedhabilita el uso de la cache general de http..
| 1 | AddType application/manifest+json .webmanifest |
| 2 | |
| 3 | ExpiresActive On |
| 4 | |
| 5 | Header set Cache-Control "max-age=1, must-revalidate" |
| 6 |
Este archivo ayuda a detectar errores en los archivos del proyecto.
Lo utiliza principalmente Visual Studio Code.
No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.
| 1 |
{
|
| 2 |
"compilerOptions": {
|
| 3 |
"checkJs": true, |
| 4 |
"strictNullChecks": true, |
| 5 |
"target": "ES6", |
| 6 |
"module": "Node16", |
| 7 |
"moduleResolution": "Node16", |
| 8 |
"lib": [
|
| 9 |
"ES2017",
|
| 10 |
"WebWorker",
|
| 11 |
"DOM"
|
| 12 |
] |
| 18 |
} |
En esta lección se muestra un ejemplo de sincronización de bases de datos.