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 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 ves 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 sin incluir el archivo
.htaccess
. En algunos casos puedes usar
filezilla
(https://filezilla-project.org/)
En algunos host como InfinityFree, tienes que configurar el certificado SSL.
En algunos host, como InfinityFree, debes subir el
archivo .htaccess
cuando el certificado SSL se ha creado e
instalado. Sirve para forzar el uso de https, para eliminar el
control de cache, pues ahora lo lleva el service worker y para asignar el
mime type correcto para el archivo de manifest.
Abre un navegador y prueba el proyecto en tu hosting.
En el hosting InfinityFree, la primera ves 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 src="js/registraServiceWorker.js"></script> |
13 | <script type="module" src="lib/js/muestraError.js"></script> |
14 | <script type="module" src="js/bd/pasatiempoConsultaNoEliminados.js"></script> |
15 | <script type="module" src="js/renderiza.js"></script> |
16 | <script type="module" src="js/sincroniza.js"></script> |
17 | <script type="module" src="js/esperaUnPocoYSincroniza.js"></script> |
18 | |
19 | </head> |
20 | |
21 | <body onload="pasatiempoConsultaNoEliminados() |
22 | .then(pasatiempos => { |
23 | renderiza(lista, pasatiempos) |
24 | sincroniza(lista) |
25 | }) |
26 | .catch(error => { |
27 | muestraError(error) |
28 | esperaUnPocoYSincroniza(lista) |
29 | })"> |
30 | |
31 | <h1>Sincronizacion</h1> |
32 | |
33 | <p><a href="agrega.html">Agregar</a></p> |
34 | |
35 | <ul id="lista"> |
36 | <li><progress max="100">Cargando…</progress></li> |
37 | </ul> |
38 | |
39 | </body> |
40 | |
41 | </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/configura.js"></script> |
12 | <script type="module" src="lib/js/muestraError.js"></script> |
13 | <script type="module" src="js/bd/pasatiempoAgrega.js"></script> |
14 | |
15 | </head> |
16 | |
17 | <body> |
18 | |
19 | <form id="forma" onsubmit=" |
20 | event.preventDefault() |
21 | // Lee el nombre, quitándole los espacios al inicio y al final. |
22 | const PAS_NOMBRE = forma.nombre.value.trim() |
23 | const modelo = { PAS_NOMBRE } |
24 | pasatiempoAgrega(modelo) |
25 | .then(json => location.href = 'index.html') |
26 | .catch(muestraError)"> |
27 | |
28 | <h1>Agregar</h1> |
29 | |
30 | <p><a href="index.html">Cancelar</a></p> |
31 | |
32 | <p> |
33 | <label> |
34 | Nombre * |
35 | <input name="nombre"> |
36 | </label> |
37 | </p> |
38 | <p>* Obligatorio</p> |
39 | <p><button type="submit">Agregar</button></p> |
40 | |
41 | </form> |
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/configura.js"></script> |
12 | <script type="module" src="lib/js/muestraError.js"></script> |
13 | <script type="module" src="lib/js/muestraObjeto.js"></script> |
14 | <script type="module" src="js/bd/pasatiempoBusca.js"></script> |
15 | <script type="module" src="js/bd/pasatiempoElimina.js"></script> |
16 | <script type="module" src="js/bd/pasatiempoModifica.js"></script> |
17 | |
18 | <script> |
19 | |
20 | // Obtiene los parámetros de la página. |
21 | const parametros = new URL(location.href).searchParams |
22 | |
23 | const paramId = parametros.get("id") |
24 | |
25 | </script> |
26 | |
27 | </head> |
28 | |
29 | <body onload="if (paramId !== null) { |
30 | pasatiempoBusca(paramId) |
31 | .then(pasatiempo => { |
32 | if (pasatiempo === undefined) throw new Error('Pasatiempo no encontrado.') |
33 | muestraObjeto(forma, { nombre: { value: pasatiempo.PAS_NOMBRE } }) |
34 | }) |
35 | .catch(muestraError) |
36 | }"> |
37 | |
38 | <form id="forma" onsubmit=" |
39 | event.preventDefault() |
40 | if (paramId !== null) { |
41 | const PAS_ID = paramId |
42 | // Lee el nombre, quitándole los espacios al inicio y al final. |
43 | const PAS_NOMBRE = forma.nombre.value.trim() |
44 | const modelo = { PAS_ID, PAS_NOMBRE } |
45 | pasatiempoModifica(modelo) |
46 | .then(json => location.href = 'index.html') |
47 | .catch(muestraError) |
48 | }"> |
49 | |
50 | <h1>Modificar</h1> |
51 | |
52 | <p><a href="index.html">Cancelar</a></p> |
53 | |
54 | <p> |
55 | <label> |
56 | Nombre * |
57 | <input name="nombre" value="Cargando…"> |
58 | </label> |
59 | </p> |
60 | |
61 | <p>* Obligatorio</p> |
62 | |
63 | <p> |
64 | |
65 | <button type="submit">Guardar</button> |
66 | |
67 | <button type="button" onclick=" |
68 | if (paramId !== null && confirm('Confirma la eliminación')) { |
69 | pasatiempoElimina(paramId) |
70 | .then(() => location.href = 'index.html') |
71 | .catch(muestraError) |
72 | }"> |
73 | Eliminar |
74 | </button> |
75 | |
76 | </p> |
77 | |
78 | </form> |
79 | |
80 | </body> |
81 | |
82 | </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 | * el archivo .vscode/settings.json, |
16 | * el archivo .htaccess, |
17 | * el archivo archivos.txt, |
18 | * este archivo (instruccionesListadoSw.txt), |
19 | * el archivo jsconfig.json, |
20 | * el archivo sw.js, |
21 | * el archivo de la base de datos, que termina en ".bd" y |
22 | está en la carpeta srv, |
23 | * todos los archivos de php y |
24 | * las líneas en blanco del final |
25 | |
26 | 5. Cambia los \ por / desde Visual Studio Code con las siguientes |
27 | combinaciones de teclas: |
28 | |
29 | Ctrl+H En el diálogo que aparece introduce lo siguiente: |
30 | Find:\ |
31 | Replace:/ |
32 | |
33 | Clic en el icono Reemplaza todo o Replace All y luego teclea ESC |
34 | |
35 | 6. Coloca las comillas y coma del final de cada línea desde Visual |
36 | Studio Code con las siguientes combinaciones de teclas: |
37 | |
38 | Ctrl+H En el diálogo que aparece, selecciona el botón |
39 | ".*" |
40 | e introduce lo siguiente: |
41 | Find:\s*$ |
42 | Replace:", |
43 | |
44 | Clic en el icono Reemplaza todo o Replace All y luego teclea ESC |
45 | |
46 | 7. Marca la carpeta inicial, presiona la combinación de teclas: |
47 | |
48 | Shift+Ctrl+L |
49 | |
50 | borra la selección, teclea " y luego ESC |
51 | |
52 | 8. Cambia las secuencias de espacios por / con las siguientes |
53 | combinaciones de teclas: |
54 | |
55 | Ctrl+H En el diálogo que aparece, selecciona el botón |
56 | ".*" |
57 | e introduce lo siguiente: |
58 | Find:\s+ |
59 | Replace:/ |
60 | |
61 | Clic en el icono Reemplaza todo o Replace All y luego teclea ESC |
62 | |
63 | 9. Cambia las "/ por " con las siguientes combinaciones de teclas: |
64 | |
65 | Ctrl+H En el diálogo que aparece, quita la selección del botón |
66 | ".*" |
67 | e introduce lo siguiente: |
68 | Find:"/ |
69 | Replace:" |
70 | |
71 | Clic en el icono Reemplaza todo o Replace All y luego teclea ESC |
72 | |
73 | 10. Copia el texto al archivo |
74 | sw.js |
75 | en el contenido del arreglo llamado ARCHIVOS, pero recuerda |
76 | mantener el último elemento, que dice: |
77 | "/" |
1 | "agrega.html", |
2 | "index.html", |
3 | "modifica.html", |
4 | "error/datosnojson.html", |
5 | "error/eliminadoincorrecto.html", |
6 | "error/errorinterno.html", |
7 | "error/faltaid.html", |
8 | "error/faltanombre.html", |
9 | "error/idincorrecto.html", |
10 | "error/modificacionincorrecta.html", |
11 | "error/nombreenblanco.html", |
12 | "error/nombreincorrecto.html", |
13 | "error/resultadonojson.html", |
14 | "js/esperaUnPocoYSincroniza.js", |
15 | "js/registraServiceWorker.js", |
16 | "js/renderiza.js", |
17 | "js/sincroniza.js", |
18 | "js/bd/Bd.js", |
19 | "js/bd/pasatiempoAgrega.js", |
20 | "js/bd/pasatiempoBusca.js", |
21 | "js/bd/pasatiempoConsultaNoEliminados.js", |
22 | "js/bd/pasatiempoConsultaTodos.js", |
23 | "js/bd/pasatiempoElimina.js", |
24 | "js/bd/pasatiempoModifica.js", |
25 | "js/bd/pasatiemposReemplaza.js", |
26 | "js/modelo/PASATIEMPO.js", |
27 | "js/modelo/validaId.js", |
28 | "js/modelo/validaNombre.js", |
29 | "js/modelo/validaPasatiempo.js", |
30 | "js/modelo/validaPasatiempos.js", |
31 | "lib/js/bdConsulta.js", |
32 | "lib/js/bdEjecuta.js", |
33 | "lib/js/consumeJson.js", |
34 | "lib/js/creaIdCliente.js", |
35 | "lib/js/enviaJson.js", |
36 | "lib/js/exportaAHtml.js", |
37 | "lib/js/htmlentities.js", |
38 | "lib/js/muestraError.js", |
39 | "lib/js/muestraObjeto.js", |
40 | "lib/js/ProblemDetails.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 | "error/datosnojson.html", |
33 | "error/eliminadoincorrecto.html", |
34 | "error/errorinterno.html", |
35 | "error/faltaid.html", |
36 | "error/faltanombre.html", |
37 | "error/idincorrecto.html", |
38 | "error/modificacionincorrecta.html", |
39 | "error/nombreenblanco.html", |
40 | "error/nombreincorrecto.html", |
41 | "error/resultadonojson.html", |
42 | "js/esperaUnPocoYSincroniza.js", |
43 | "js/registraServiceWorker.js", |
44 | "js/renderiza.js", |
45 | "js/sincroniza.js", |
46 | "js/bd/Bd.js", |
47 | "js/bd/pasatiempoAgrega.js", |
48 | "js/bd/pasatiempoBusca.js", |
49 | "js/bd/pasatiempoConsultaNoEliminados.js", |
50 | "js/bd/pasatiempoConsultaTodos.js", |
51 | "js/bd/pasatiempoElimina.js", |
52 | "js/bd/pasatiempoModifica.js", |
53 | "js/bd/pasatiemposReemplaza.js", |
54 | "js/modelo/PASATIEMPO.js", |
55 | "js/modelo/validaId.js", |
56 | "js/modelo/validaNombre.js", |
57 | "js/modelo/validaPasatiempo.js", |
58 | "js/modelo/validaPasatiempos.js", |
59 | "lib/js/bdConsulta.js", |
60 | "lib/js/bdEjecuta.js", |
61 | "lib/js/consumeJson.js", |
62 | "lib/js/creaIdCliente.js", |
63 | "lib/js/enviaJson.js", |
64 | "lib/js/exportaAHtml.js", |
65 | "lib/js/htmlentities.js", |
66 | "lib/js/muestraError.js", |
67 | "lib/js/muestraObjeto.js", |
68 | "lib/js/ProblemDetails.js", |
69 | "/" |
70 | ] |
71 | |
72 | // Verifica si el código corre dentro de un service worker. |
73 | if (self instanceof ServiceWorkerGlobalScope) { |
74 | // Evento al empezar a instalar el servide worker, |
75 | self.addEventListener("install", |
76 | (/** @type {ExtendableEvent} */ evt) => { |
77 | console.log("El service worker se está instalando.") |
78 | evt.waitUntil(llenaElCache()) |
79 | }) |
80 | |
81 | // Evento al solicitar información a la red. |
82 | self.addEventListener("fetch", (/** @type {FetchEvent} */ evt) => { |
83 | if (evt.request.method === "GET") { |
84 | evt.respondWith(buscaLaRespuestaEnElCache(evt)) |
85 | } |
86 | }) |
87 | |
88 | // Evento cuando el service worker se vuelve activo. |
89 | self.addEventListener("activate", |
90 | () => console.log("El service worker está activo.")) |
91 | } |
92 | |
93 | async function llenaElCache() { |
94 | console.log("Intentando cargar caché:", CACHE) |
95 | // Borra todos los cachés. |
96 | const keys = await caches.keys() |
97 | for (const key of keys) { |
98 | await caches.delete(key) |
99 | } |
100 | // Abre el caché de este service worker. |
101 | const cache = await caches.open(CACHE) |
102 | // Carga el listado de ARCHIVOS. |
103 | await cache.addAll(ARCHIVOS) |
104 | console.log("Cache cargado:", CACHE) |
105 | console.log("Versión:", VERSION) |
106 | } |
107 | |
108 | /** @param {FetchEvent} evt */ |
109 | async function buscaLaRespuestaEnElCache(evt) { |
110 | // Abre el caché. |
111 | const cache = await caches.open(CACHE) |
112 | const request = evt.request |
113 | /* Busca la respuesta a la solicitud en el contenido del caché, sin |
114 | * tomar en cuenta la parte después del símbolo "?" en la URL. */ |
115 | const response = await cache.match(request, { ignoreSearch: true }) |
116 | if (response === undefined) { |
117 | /* Si no la encuentra, empieza a descargar de la red y devuelve |
118 | * la promesa. */ |
119 | return fetch(request) |
120 | } else { |
121 | // Si la encuentra, devuelve la respuesta encontrada en el caché. |
122 | return response |
123 | } |
124 | } |
1 | import { exportaAHtml } from "../lib/js/exportaAHtml.js" |
2 | import { sincroniza } from "./sincroniza.js" |
3 | |
4 | /** |
5 | * Cada 20 segundos (2000 milisegundos) después de la última |
6 | * sincronización, los datos se envían al servidor para volver a |
7 | * sincronizarse con los datos del servidor. |
8 | */ |
9 | const MILISEGUNDOS_PARA_VOLVER_A_SINCRONIZAR = 20000 |
10 | |
11 | /** |
12 | * @param {HTMLUListElement} lista |
13 | */ |
14 | export function esperaUnPocoYSincroniza(lista) { |
15 | setTimeout(() => sincroniza(lista), MILISEGUNDOS_PARA_VOLVER_A_SINCRONIZAR) |
16 | } |
17 | |
18 | exportaAHtml(esperaUnPocoYSincroniza) |
1 | "use strict" // usa JavaScript en modo estricto. |
2 | |
3 | const nombreDeServiceWorker = "sw.js" |
4 | |
5 | try { |
6 | navigator.serviceWorker.register(nombreDeServiceWorker) |
7 | .then(registro => { |
8 | console.log(nombreDeServiceWorker, "registrado.") |
9 | console.log(registro) |
10 | }) |
11 | .catch(error => console.log(error)) |
12 | } catch (error) { |
13 | console.log(error) |
14 | } |
1 | import { exportaAHtml } from "../lib/js/exportaAHtml.js" |
2 | import { htmlentities } from "../lib/js/htmlentities.js" |
3 | |
4 | /** |
5 | * @param {HTMLUListElement} lista |
6 | * @param {import("./modelo/PASATIEMPO.js").PASATIEMPO[]} pasatiempos |
7 | */ |
8 | export function renderiza(lista, pasatiempos) { |
9 | let render = "" |
10 | for (const modelo of pasatiempos) { |
11 | if (modelo.PAS_ID === undefined) |
12 | throw new Error(`Falta PAS_ID de ${modelo.PAS_NOMBRE}.`) |
13 | const nombre = htmlentities(modelo.PAS_NOMBRE) |
14 | const searchParams = new URLSearchParams([["id", modelo.PAS_ID]]) |
15 | const params = htmlentities(searchParams.toString()) |
16 | render += /* html */ |
17 | `<li> |
18 | <p><a href="modifica.html?${params}">${nombre}</a></p> |
19 | </li>` |
20 | } |
21 | lista.innerHTML = render |
22 | } |
23 | |
24 | exportaAHtml(renderiza) |
1 | import { enviaJson } from "../lib/js/enviaJson.js" |
2 | import { exportaAHtml } from "../lib/js/exportaAHtml.js" |
3 | import { muestraError } from "../lib/js/muestraError.js" |
4 | import { pasatiempoConsultaTodos } from "./bd/pasatiempoConsultaTodos.js" |
5 | import { pasatiemposReemplaza } from "./bd/pasatiemposReemplaza.js" |
6 | import { esperaUnPocoYSincroniza } from "./esperaUnPocoYSincroniza.js" |
7 | import { validaPasatiempos } from "./modelo/validaPasatiempos.js" |
8 | import { renderiza } from "./renderiza.js" |
9 | |
10 | /** |
11 | * @param {HTMLUListElement} lista |
12 | */ |
13 | export async function sincroniza(lista) { |
14 | try { |
15 | if (navigator.onLine) { |
16 | const todos = await pasatiempoConsultaTodos() |
17 | const respuesta = await enviaJson("srv/sincroniza.php", todos) |
18 | const pasatiempos = validaPasatiempos(respuesta.body) |
19 | await pasatiemposReemplaza(pasatiempos) |
20 | renderiza(lista, pasatiempos) |
21 | } |
22 | } catch (error) { |
23 | muestraError(error) |
24 | } |
25 | esperaUnPocoYSincroniza(lista) |
26 | |
27 | } |
28 | |
29 | exportaAHtml(sincroniza) |
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 = "sincronizacion" |
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 { bdEjecuta } from "../../lib/js/bdEjecuta.js" |
2 | import { creaIdCliente } from "../../lib/js/creaIdCliente.js" |
3 | import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js" |
4 | import { validaNombre } from "../modelo/validaNombre.js" |
5 | import { exportaAHtml } from "../../lib/js/exportaAHtml.js" |
6 | |
7 | /** |
8 | * @param {import("../modelo/PASATIEMPO.js").PASATIEMPO} modelo |
9 | */ |
10 | export async function pasatiempoAgrega(modelo) { |
11 | validaNombre(modelo.PAS_NOMBRE) |
12 | modelo.PAS_MODIFICACION = Date.now() |
13 | modelo.PAS_ELIMINADO = 0 |
14 | // Genera id único en internet. |
15 | modelo.PAS_ID = creaIdCliente(Date.now().toString()) |
16 | return bdEjecuta(Bd, [ALMACEN_PASATIEMPO], transaccion => { |
17 | const almacenPasatiempo = transaccion.objectStore(ALMACEN_PASATIEMPO) |
18 | almacenPasatiempo.add(modelo) |
19 | }) |
20 | } |
21 | |
22 | exportaAHtml(pasatiempoAgrega) |
1 | import { bdConsulta } from "../../lib/js/bdConsulta.js" |
2 | import { exportaAHtml } from "../../lib/js/exportaAHtml.js" |
3 | import { validaPasatiempo } from "../modelo/validaPasatiempo.js" |
4 | import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js" |
5 | |
6 | /** |
7 | * @param {string} id |
8 | */ |
9 | export async function pasatiempoBusca(id) { |
10 | |
11 | return bdConsulta(Bd, [ALMACEN_PASATIEMPO], |
12 | /** |
13 | * @param {(resultado: import("../modelo/PASATIEMPO.js").PASATIEMPO|undefined) |
14 | * => any} resolve |
15 | */ |
16 | (transaccion, resolve) => { |
17 | |
18 | /* Pide el primer objeto de ALMACEN_PASATIEMPO que tenga como llave |
19 | * primaria el valor del parámetro id. */ |
20 | const consulta = transaccion.objectStore(ALMACEN_PASATIEMPO).get(id) |
21 | |
22 | // onsuccess se invoca solo una vez, devolviendo el objeto solicitado. |
23 | consulta.onsuccess = () => { |
24 | /* Se recupera el objeto solicitado usando |
25 | * consulta.result |
26 | * Si el objeto no se encuentra se recupera undefined. */ |
27 | const objeto = consulta.result |
28 | if (objeto !== undefined) { |
29 | const modelo = validaPasatiempo(objeto) |
30 | if (modelo.PAS_ELIMINADO === 0) { |
31 | resolve(modelo) |
32 | return |
33 | } |
34 | } |
35 | resolve(undefined) |
36 | |
37 | } |
38 | |
39 | }) |
40 | |
41 | } |
42 | |
43 | exportaAHtml(pasatiempoBusca) |
1 | import { bdConsulta } from "../../lib/js/bdConsulta.js" |
2 | import { exportaAHtml } from "../../lib/js/exportaAHtml.js" |
3 | import { validaPasatiempo } from "../modelo/validaPasatiempo.js" |
4 | import { ALMACEN_PASATIEMPO, Bd, INDICE_NOMBRE } from "./Bd.js" |
5 | |
6 | export async function pasatiempoConsultaNoEliminados() { |
7 | |
8 | return bdConsulta(Bd, [ALMACEN_PASATIEMPO], |
9 | /** |
10 | * @param {(resultado: import("../modelo/PASATIEMPO.js").PASATIEMPO[])=>void |
11 | * } resolve |
12 | */ |
13 | (transaccion, resolve) => { |
14 | |
15 | const resultado = [] |
16 | |
17 | const almacenPasatiempo = transaccion.objectStore(ALMACEN_PASATIEMPO) |
18 | |
19 | // Usa el índice INDICE_NOMBRE para recuperar los datos ordenados. |
20 | const indiceNombre = almacenPasatiempo.index(INDICE_NOMBRE) |
21 | |
22 | // Pide un cursor para recorrer cada objeto que devuelve la consulta. |
23 | const consulta = indiceNombre.openCursor() |
24 | |
25 | /* onsuccess se invoca por cada uno de los objetos de la consulta y una vez |
26 | * cuando se acaban dichos objetos. */ |
27 | consulta.onsuccess = () => { |
28 | /* El cursor correspondiente al objeto se recupera usando |
29 | * consulta.result */ |
30 | const cursor = consulta.result |
31 | if (cursor === null) { |
32 | /* Si el cursor vale null, ya no hay más objetos que procesar; por lo |
33 | * mismo, se devuelve el resultado con los pasatiempos recuperados, usando |
34 | * resolve(resultado). */ |
35 | resolve(resultado) |
36 | } else { |
37 | /* Si el cursor no vale null y hay más objetos, el siguiente se obtiene con |
38 | * cursor.value */ |
39 | const modelo = validaPasatiempo(cursor.value) |
40 | if (modelo.PAS_ELIMINADO === 0) { |
41 | resultado.push(modelo) |
42 | } |
43 | /* Busca el siguiente objeto de la consulta, que se recupera la siguiente |
44 | * vez que se invoque la función onsuccess. */ |
45 | cursor.continue() |
46 | } |
47 | } |
48 | |
49 | }) |
50 | |
51 | } |
52 | |
53 | exportaAHtml(pasatiempoConsultaNoEliminados) |
1 | import { bdConsulta } from "../../lib/js/bdConsulta.js" |
2 | import { validaPasatiempo } from "../modelo/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("../modelo/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 { bdEjecuta } from "../../lib/js/bdEjecuta.js" |
2 | import { exportaAHtml } from "../../lib/js/exportaAHtml.js" |
3 | import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js" |
4 | import { pasatiempoBusca } from "./pasatiempoBusca.js" |
5 | |
6 | /** |
7 | * @param { string } id |
8 | */ |
9 | export async function pasatiempoElimina(id) { |
10 | const modelo = await pasatiempoBusca(id) |
11 | if (modelo !== undefined) { |
12 | modelo.PAS_MODIFICACION = Date.now() |
13 | modelo.PAS_ELIMINADO = 1 |
14 | return bdEjecuta(Bd, [ALMACEN_PASATIEMPO], transaccion => { |
15 | const almacenPasatiempo = transaccion.objectStore(ALMACEN_PASATIEMPO) |
16 | almacenPasatiempo.put(modelo) |
17 | }) |
18 | } |
19 | } |
20 | |
21 | exportaAHtml(pasatiempoElimina) |
1 | import { bdEjecuta } from "../../lib/js/bdEjecuta.js" |
2 | import { exportaAHtml } from "../../lib/js/exportaAHtml.js" |
3 | import { validaId } from "../modelo/validaId.js" |
4 | import { validaNombre } from "../modelo/validaNombre.js" |
5 | import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js" |
6 | import { pasatiempoBusca } from "./pasatiempoBusca.js" |
7 | |
8 | /** |
9 | * @param { import("../modelo/PASATIEMPO.js").PASATIEMPO } modelo |
10 | */ |
11 | export async function pasatiempoModifica(modelo) { |
12 | validaNombre(modelo.PAS_NOMBRE) |
13 | if (modelo.PAS_ID === undefined) |
14 | throw new Error(`Falta PAS_ID de ${modelo.PAS_NOMBRE}.`) |
15 | validaId(modelo.PAS_ID) |
16 | const anterior = await pasatiempoBusca(modelo.PAS_ID) |
17 | if (anterior !== undefined) { |
18 | modelo.PAS_MODIFICACION = Date.now() |
19 | modelo.PAS_ELIMINADO = 0 |
20 | return bdEjecuta(Bd, [ALMACEN_PASATIEMPO], transaccion => { |
21 | const almacenPasatiempo = transaccion.objectStore(ALMACEN_PASATIEMPO) |
22 | almacenPasatiempo.put(modelo) |
23 | }) |
24 | } |
25 | } |
26 | |
27 | exportaAHtml(pasatiempoModifica) |
1 | import { bdEjecuta } from "../../lib/js/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("../modelo/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 | /** |
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 | /** |
2 | * @param {string} id |
3 | */ |
4 | export function validaId(id) { |
5 | if (id === "") |
6 | throw new Error("Falta el id.") |
7 | } |
1 | /** |
2 | * @param {string} nombre |
3 | */ |
4 | export function validaNombre(nombre) { |
5 | if (nombre === "") |
6 | throw new Error("Falta el nombre.") |
7 | } |
1 | /** |
2 | * @param { any } objeto |
3 | * @returns {import("./PASATIEMPO.js").PASATIEMPO} |
4 | */ |
5 | export function validaPasatiempo(objeto) { |
6 | |
7 | if (typeof objeto.PAS_ID !== "string") |
8 | throw new Error("El id debe ser texto.") |
9 | |
10 | if (typeof objeto.PAS_NOMBRE !== "string") |
11 | throw new Error("El nombre debe ser texto.") |
12 | |
13 | if (typeof objeto.PAS_MODIFICACION !== "number") |
14 | throw new Error("El campo modificacion debe ser número.") |
15 | |
16 | if (typeof objeto.PAS_ELIMINADO !== "number") |
17 | throw new Error("El campo eliminado debe ser número.") |
18 | |
19 | return objeto |
20 | |
21 | } |
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 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/ejecutaServicio.php"; |
4 | require_once __DIR__ . "/../lib/php/recuperaJson.php"; |
5 | require_once __DIR__ . "/../lib/php/devuelveJson.php"; |
6 | require_once __DIR__ . "/../lib/php/ProblemDetails.php"; |
7 | require_once __DIR__ . "/../lib/php/devuelveProblemDetails.php"; |
8 | require_once __DIR__ . "/../lib/php/devuelveErrorInterno.php"; |
9 | require_once __DIR__ . "/modelo/TABLA_PASATIEMPO.php"; |
10 | require_once __DIR__ . "/modelo/validaPasatiempo.php"; |
11 | require_once __DIR__ . "/bd/pasatiempoAgrega.php"; |
12 | require_once __DIR__ . "/bd/pasatiempoBusca.php"; |
13 | require_once __DIR__ . "/bd/pasatiempoConsultaNoEliminados.php"; |
14 | require_once __DIR__ . "/bd/pasatiempoModifica.php"; |
15 | |
16 | ejecutaServicio(function () { |
17 | |
18 | $lista = recuperaJson(); |
19 | |
20 | if (!is_array($lista)) { |
21 | $lista = []; |
22 | } |
23 | |
24 | foreach ($lista as $modelo) { |
25 | $modeloEnElCliente = validaPasatiempo($modelo); |
26 | $modeloEnElServidor = pasatiempoBusca($modeloEnElCliente[PAS_ID]); |
27 | |
28 | if ($modeloEnElServidor === false) { |
29 | |
30 | /* CONFLICTO: El modelo no ha estado en el servidor. |
31 | * AGREGARLO solamente si no está eliminado. */ |
32 | if ($modeloEnElCliente[PAS_ELIMINADO] === 0) { |
33 | pasatiempoAgrega($modeloEnElCliente); |
34 | } |
35 | } elseif ( |
36 | $modeloEnElServidor[PAS_ELIMINADO] === 0 |
37 | && $modeloEnElCliente[PAS_ELIMINADO] === 1 |
38 | ) { |
39 | |
40 | /* CONFLICTO: El registro está en el servidor, donde no se ha eliminado, pero |
41 | * ha sido eliminado en el cliente. |
42 | * Gana el cliente, porque optamos por no revivir lo eliminado. */ |
43 | pasatiempoModifica($modeloEnElCliente); |
44 | } else if ( |
45 | $modeloEnElCliente[PAS_ELIMINADO] === 0 |
46 | && $modeloEnElServidor[PAS_ELIMINADO] === 0 |
47 | ) { |
48 | |
49 | /* CONFLICTO: Registros en el servidor y en el cliente. Pueden ser |
50 | * diferentes. |
51 | * GANA FECHA MÁS GRANDE. Cuando gana el servidor, no se hace nada. */ |
52 | if ( |
53 | $modeloEnElCliente[PAS_MODIFICACION] > |
54 | $modeloEnElServidor[PAS_MODIFICACION] |
55 | ) { |
56 | // La versión del cliente es más nueva y prevalece. |
57 | pasatiempoModifica($modeloEnElCliente); |
58 | } |
59 | } |
60 | } |
61 | |
62 | $lista = pasatiempoConsultaNoEliminados(); |
63 | |
64 | devuelveJson($lista); |
65 | }); |
66 |
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:sincronizacion.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__ . "/../../lib/php/validaNombre.php"; |
4 | require_once __DIR__ . "/../../lib/php/insert.php"; |
5 | require_once __DIR__ . "/Bd.php"; |
6 | require_once __DIR__ . "/../modelo/TABLA_PASATIEMPO.php"; |
7 | require_once __DIR__ . "/../modelo/validaId.php"; |
8 | |
9 | /** |
10 | * @param array{ |
11 | * PAS_ID: string, |
12 | * PAS_NOMBRE: string, |
13 | * PAS_MODIFICACION: int, |
14 | * PAS_ELIMINADO: int |
15 | * } $modelo |
16 | */ |
17 | function pasatiempoAgrega(array $modelo) |
18 | { |
19 | validaId($modelo[PAS_ID]); |
20 | validaNombre($modelo[PAS_NOMBRE]); |
21 | insert(pdo: Bd::pdo(), into: PASATIEMPO, values: $modelo); |
22 | } |
23 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../../lib/php/selectFirst.php"; |
4 | require_once __DIR__ . "/Bd.php"; |
5 | require_once __DIR__ . "/../modelo/TABLA_PASATIEMPO.php"; |
6 | |
7 | /** |
8 | * @return false | array{ |
9 | * PAS_ID: string, |
10 | * PAS_NOMBRE: string, |
11 | * PAS_MODIFICACION: int, |
12 | * PAS_ELIMINADO: int |
13 | * } |
14 | */ |
15 | function pasatiempoBusca(string $id): false|array |
16 | { |
17 | return selectFirst( |
18 | pdo: Bd::pdo(), |
19 | from: PASATIEMPO, |
20 | where: [PAS_ID => $id] |
21 | ); |
22 | } |
23 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../../lib/php/select.php"; |
4 | require_once __DIR__ . "/Bd.php"; |
5 | require_once __DIR__ . "/../modelo/TABLA_PASATIEMPO.php"; |
6 | |
7 | /** |
8 | * @return array{ |
9 | * PAS_ID: string, |
10 | * PAS_NOMBRE: string, |
11 | * PAS_MODIFICACION: int, |
12 | * PAS_ELIMINADO: int |
13 | * }[] |
14 | */ |
15 | function pasatiempoConsultaNoEliminados() |
16 | { |
17 | return select( |
18 | pdo: Bd::pdo(), |
19 | from: PASATIEMPO, |
20 | where: [PAS_ELIMINADO => 0], |
21 | orderBy: PAS_NOMBRE |
22 | ); |
23 | } |
24 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../../lib/php/validaNombre.php"; |
4 | require_once __DIR__ . "/../../lib/php/update.php"; |
5 | require_once __DIR__ . "/Bd.php"; |
6 | require_once __DIR__ . "/../modelo/TABLA_PASATIEMPO.php"; |
7 | require_once __DIR__ . "/../modelo/validaId.php"; |
8 | |
9 | /** |
10 | * @param array{ |
11 | * PAS_ID: string, |
12 | * PAS_NOMBRE: string, |
13 | * PAS_MODIFICACION: int, |
14 | * PAS_ELIMINADO: int |
15 | * } $modelo |
16 | */ |
17 | function pasatiempoModifica(array $modelo) |
18 | { |
19 | validaId($modelo[PAS_ID]); |
20 | validaNombre($modelo[PAS_NOMBRE]); |
21 | update( |
22 | pdo: Bd::pdo(), |
23 | table: PASATIEMPO, |
24 | set: $modelo, |
25 | where: [PAS_ID => $modelo[PAS_ID]] |
26 | ); |
27 | } |
28 |
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 | function validaId(string $id) |
4 | { |
5 | if ($id === "") |
6 | throw new ProblemDetails( |
7 | status: BAD_REQUEST, |
8 | title: "Falta el id.", |
9 | type: "/error/faltaid.html", |
10 | ); |
11 | } |
12 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../../lib/php/BAD_REQUEST.php"; |
4 | require_once __DIR__ . "/../../lib/php/validaJson.php"; |
5 | require_once __DIR__ . "/../../lib/php/ProblemDetails.php"; |
6 | require_once __DIR__ . "/TABLA_PASATIEMPO.php"; |
7 | |
8 | function validaPasatiempo($objeto) |
9 | { |
10 | |
11 | $objeto = validaJson($objeto); |
12 | |
13 | if (!isset($objeto->PAS_ID) || !is_string($objeto->PAS_ID)) |
14 | throw new ProblemDetails( |
15 | status: BAD_REQUEST, |
16 | title: "El id debe ser texto.", |
17 | type: "/error/idincorrecto.html", |
18 | ); |
19 | |
20 | if (!isset($objeto->PAS_NOMBRE) || !is_string($objeto->PAS_NOMBRE)) |
21 | throw new ProblemDetails( |
22 | status: BAD_REQUEST, |
23 | title: "El nombre debe ser texto.", |
24 | type: "/error/nombreincorrecto.html", |
25 | ); |
26 | |
27 | if (!isset($objeto->PAS_MODIFICACION) || !is_int($objeto->PAS_MODIFICACION)) |
28 | throw new ProblemDetails( |
29 | status: BAD_REQUEST, |
30 | title: "La modificacion debe ser número.", |
31 | type: "/error/modificacionincorrecta.html", |
32 | ); |
33 | |
34 | if (!isset($objeto->PAS_ELIMINADO) || !is_int($objeto->PAS_ELIMINADO)) |
35 | throw new ProblemDetails( |
36 | status: BAD_REQUEST, |
37 | title: "El campo eliminado debe ser entero.", |
38 | type: "/error/eliminadoincorrecto.html", |
39 | ); |
40 | |
41 | return [ |
42 | PAS_ID => $objeto->PAS_ID, |
43 | PAS_NOMBRE => $objeto->PAS_NOMBRE, |
44 | PAS_MODIFICACION => $objeto->PAS_MODIFICACION, |
45 | PAS_ELIMINADO => $objeto->PAS_ELIMINADO |
46 | ]; |
47 | } |
48 |
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 { exportaAHtml } from "./exportaAHtml.js" |
2 | import { ProblemDetails } from "./ProblemDetails.js" |
3 | |
4 | /** |
5 | * Espera a que la promesa de un fetch termine. Si |
6 | * hay error, lanza una excepción. Si no hay error, |
7 | * interpreta la respuesta del servidor como JSON y |
8 | * la convierte en una literal de objeto. |
9 | * |
10 | * @param { string | Promise<Response> } servicio |
11 | */ |
12 | export async function consumeJson(servicio) { |
13 | |
14 | if (typeof servicio === "string") { |
15 | servicio = fetch(servicio, { |
16 | headers: { "Accept": "application/json, application/problem+json" } |
17 | }) |
18 | } else if (!(servicio instanceof Promise)) { |
19 | throw new Error("Servicio de tipo incorrecto.") |
20 | } |
21 | |
22 | const respuesta = await servicio |
23 | |
24 | const headers = respuesta.headers |
25 | |
26 | if (respuesta.ok) { |
27 | // Aparentemente el servidor tuvo éxito. |
28 | |
29 | if (respuesta.status === 204) { |
30 | // No contiene texto de respuesta. |
31 | |
32 | return { headers, body: {} } |
33 | |
34 | } else { |
35 | |
36 | const texto = await respuesta.text() |
37 | |
38 | try { |
39 | |
40 | return { headers, body: JSON.parse(texto) } |
41 | |
42 | } catch (error) { |
43 | |
44 | // El contenido no es JSON. Probablemente sea texto de un error. |
45 | throw new ProblemDetails(respuesta.status, headers, texto, |
46 | "/error/errorinterno.html") |
47 | |
48 | } |
49 | |
50 | } |
51 | |
52 | } else { |
53 | // Hay un error. |
54 | |
55 | const texto = await respuesta.text() |
56 | |
57 | if (texto === "") { |
58 | |
59 | // No hay texto. Se usa el texto predeterminado. |
60 | throw new ProblemDetails(respuesta.status, headers, respuesta.statusText) |
61 | |
62 | } else { |
63 | // Debiera se un ProblemDetails en JSON. |
64 | |
65 | try { |
66 | |
67 | const { title, type, detail } = JSON.parse(texto) |
68 | |
69 | throw new ProblemDetails(respuesta.status, headers, |
70 | typeof title === "string" ? title : respuesta.statusText, |
71 | typeof type === "string" ? type : undefined, |
72 | typeof detail === "string" ? detail : undefined) |
73 | |
74 | } catch (error) { |
75 | |
76 | if (error instanceof ProblemDetails) { |
77 | // El error si era un ProblemDetails |
78 | |
79 | throw error |
80 | |
81 | } else { |
82 | |
83 | throw new ProblemDetails(respuesta.status, headers, respuesta.statusText, |
84 | undefined, texto) |
85 | |
86 | } |
87 | |
88 | } |
89 | |
90 | } |
91 | |
92 | } |
93 | |
94 | } |
95 | |
96 | exportaAHtml(consumeJson) |
1 | import { exportaAHtml } from "./exportaAHtml.js" |
2 | |
3 | /** |
4 | * Añade caracteres al azar a una raíz, para obtener un clientId único. |
5 | * @param {string} raiz |
6 | */ |
7 | export function creaIdCliente(raiz) { |
8 | const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" |
9 | for (var i = 0; i < 15; i++) { |
10 | raiz += chars.charAt(Math.floor(Math.random() * chars.length)) |
11 | } |
12 | return raiz |
13 | } |
14 | |
15 | exportaAHtml(creaIdCliente) |
1 | import { consumeJson } from "./consumeJson.js" |
2 | import { exportaAHtml } from "./exportaAHtml.js" |
3 | |
4 | /** |
5 | * @param { string } url |
6 | * @param { Object } body |
7 | * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS" |
8 | * | "CONNECT" | "HEAD" } metodoHttp |
9 | */ |
10 | export async function enviaJson(url, body, metodoHttp = "POST") { |
11 | return await consumeJson(fetch(url, { |
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 | |
21 | exportaAHtml(enviaJson) |
1 | /** |
2 | * Permite que los eventos de html usen la función. |
3 | * @param {function} functionInstance |
4 | */ |
5 | export function exportaAHtml(functionInstance) { |
6 | window[nombreDeFuncionParaHtml(functionInstance)] = functionInstance |
7 | } |
8 | |
9 | /** |
10 | * @param {function} valor |
11 | */ |
12 | export function nombreDeFuncionParaHtml(valor) { |
13 | const names = valor.name.split(/\s+/g) |
14 | return names[names.length - 1] |
15 | } |
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 { exportaAHtml } from "./exportaAHtml.js" |
2 | import { ProblemDetails } from "./ProblemDetails.js" |
3 | |
4 | /** |
5 | * Muestra un error en la consola y en un cuadro de |
6 | * alerta el mensaje de una excepción. |
7 | * @param { ProblemDetails | Error | null } error descripción del error. |
8 | */ |
9 | export function muestraError(error) { |
10 | |
11 | if (error === null) { |
12 | |
13 | console.error("Error") |
14 | alert("Error") |
15 | |
16 | } else if (error instanceof ProblemDetails) { |
17 | |
18 | let mensaje = error.title |
19 | if (error.detail) { |
20 | mensaje += `\n\n${error.detail}` |
21 | } |
22 | mensaje += `\n\nCódigo: ${error.status}` |
23 | if (error.type) { |
24 | mensaje += ` ${error.type}` |
25 | } |
26 | |
27 | console.error(mensaje) |
28 | console.error(error) |
29 | console.error("Headers:") |
30 | error.headers.forEach((valor, llave) => console.error(llave, "=", valor)) |
31 | alert(mensaje) |
32 | |
33 | } else { |
34 | |
35 | console.error(error) |
36 | alert(error.message) |
37 | |
38 | } |
39 | |
40 | } |
41 | |
42 | exportaAHtml(muestraError) |
1 | import { exportaAHtml } from "./exportaAHtml.js" |
2 | |
3 | /** |
4 | * @param { Document | HTMLElement } raizHtml |
5 | * @param { any } objeto |
6 | */ |
7 | export function muestraObjeto(raizHtml, objeto) { |
8 | |
9 | for (const [nombre, definiciones] of Object.entries(objeto)) { |
10 | |
11 | if (Array.isArray(definiciones)) { |
12 | |
13 | muestraArray(raizHtml, nombre, definiciones) |
14 | |
15 | } else if (definiciones !== undefined && definiciones !== null) { |
16 | |
17 | const elementoHtml = buscaElementoHtml(raizHtml, nombre) |
18 | |
19 | if (elementoHtml instanceof HTMLInputElement) { |
20 | |
21 | muestraInput(raizHtml, elementoHtml, definiciones) |
22 | |
23 | } else if (elementoHtml !== null) { |
24 | |
25 | for (const [atributo, valor] of Object.entries(definiciones)) { |
26 | if (atributo in elementoHtml) { |
27 | elementoHtml[atributo] = valor |
28 | } |
29 | } |
30 | |
31 | } |
32 | |
33 | } |
34 | |
35 | } |
36 | |
37 | } |
38 | exportaAHtml(muestraObjeto) |
39 | |
40 | /** |
41 | * @param { Document | HTMLElement } raizHtml |
42 | * @param { string } nombre |
43 | */ |
44 | export function buscaElementoHtml(raizHtml, nombre) { |
45 | return raizHtml.querySelector( |
46 | `#${nombre},[name="${nombre}"],[data-name="${nombre}"]`) |
47 | } |
48 | |
49 | /** |
50 | * @param { Document | HTMLElement } raizHtml |
51 | * @param { string } propiedad |
52 | * @param {any[]} valores |
53 | */ |
54 | function muestraArray(raizHtml, propiedad, valores) { |
55 | |
56 | const conjunto = new Set(valores) |
57 | const elementos = |
58 | raizHtml.querySelectorAll(`[name="${propiedad}"],[data-name="${propiedad}"]`) |
59 | |
60 | if (elementos.length === 1) { |
61 | const elemento = elementos[0] |
62 | |
63 | if (elemento instanceof HTMLSelectElement) { |
64 | const options = elemento.options |
65 | for (let i = 0, len = options.length; i < len; i++) { |
66 | const option = options[i] |
67 | option.selected = conjunto.has(option.value) |
68 | } |
69 | return |
70 | } |
71 | |
72 | } |
73 | |
74 | for (let i = 0, len = elementos.length; i < len; i++) { |
75 | const elemento = elementos[i] |
76 | if (elemento instanceof HTMLInputElement) { |
77 | elemento.checked = conjunto.has(elemento.value) |
78 | } |
79 | } |
80 | |
81 | } |
82 | |
83 | /** |
84 | * @param { Document | HTMLElement } raizHtml |
85 | * @param { HTMLInputElement } input |
86 | * @param { any } definiciones |
87 | */ |
88 | function muestraInput(raizHtml, input, definiciones) { |
89 | |
90 | for (const [atributo, valor] of Object.entries(definiciones)) { |
91 | |
92 | if (atributo == "data-file") { |
93 | |
94 | const img = getImgParaElementoHtml(raizHtml, input) |
95 | if (img !== null) { |
96 | input.dataset.file = valor |
97 | input.value = "" |
98 | if (valor === "") { |
99 | img.src = "" |
100 | img.hidden = true |
101 | } else { |
102 | img.src = valor |
103 | img.hidden = false |
104 | } |
105 | } |
106 | |
107 | } else if (atributo in input) { |
108 | |
109 | input[atributo] = valor |
110 | |
111 | } |
112 | } |
113 | |
114 | } |
115 | |
116 | /** |
117 | * @param { Document | HTMLElement } raizHtml |
118 | * @param { HTMLElement } elementoHtml |
119 | */ |
120 | export function getImgParaElementoHtml(raizHtml, elementoHtml) { |
121 | const imgId = elementoHtml.getAttribute("data-img") |
122 | if (imgId === null) { |
123 | return null |
124 | } else { |
125 | const input = buscaElementoHtml(raizHtml, imgId) |
126 | if (input instanceof HTMLImageElement) { |
127 | return input |
128 | } else { |
129 | return null |
130 | } |
131 | } |
132 | } |
1 | /** |
2 | * Detalle de los errores devueltos por un servicio. |
3 | */ |
4 | export class ProblemDetails extends Error { |
5 | |
6 | /** |
7 | * @param {number} status |
8 | * @param {Headers} headers |
9 | * @param {string} title |
10 | * @param {string} [type] |
11 | * @param {string} [detail] |
12 | */ |
13 | constructor(status, headers, title, type, detail) { |
14 | super(title) |
15 | /** |
16 | * @readonly |
17 | */ |
18 | this.status = status |
19 | /** |
20 | * @readonly |
21 | */ |
22 | this.headers = headers |
23 | /** |
24 | * @readonly |
25 | */ |
26 | this.type = type |
27 | /** |
28 | * @readonly |
29 | */ |
30 | this.detail = detail |
31 | /** |
32 | * @readonly |
33 | */ |
34 | this.title = title |
35 | } |
36 | |
37 | } |
1 | <?php |
2 | |
3 | const BAD_REQUEST = 400; |
4 |
1 | <?php |
2 | |
3 | function calculaArregloDeParametros(array $arreglo) |
4 | { |
5 | $parametros = []; |
6 | foreach ($arreglo as $llave => $valor) { |
7 | $parametros[":$llave"] = $valor; |
8 | } |
9 | return $parametros; |
10 | } |
11 |
1 | <?php |
2 | |
3 | function calculaSqlDeAsignaciones(string $separador, array $arreglo) |
4 | { |
5 | $primerElemento = true; |
6 | $sqlDeAsignacion = ""; |
7 | foreach ($arreglo as $llave => $valor) { |
8 | $sqlDeAsignacion .= |
9 | ($primerElemento === true ? "" : $separador) . "$llave=:$llave"; |
10 | $primerElemento = false; |
11 | } |
12 | return $sqlDeAsignacion; |
13 | } |
14 |
1 | <?php |
2 | |
3 | function calculaSqlDeCamposDeInsert(array $values) |
4 | { |
5 | $primerCampo = true; |
6 | $sqlDeCampos = ""; |
7 | foreach ($values as $nombreDeValue => $valorDeValue) { |
8 | $sqlDeCampos .= ($primerCampo === true ? "" : ",") . "$nombreDeValue"; |
9 | $primerCampo = false; |
10 | } |
11 | return $sqlDeCampos; |
12 | } |
13 |
1 | <?php |
2 | |
3 | function calculaSqlDeValues(array $values) |
4 | { |
5 | $primerValue = true; |
6 | $sqlDeValues = ""; |
7 | foreach ($values as $nombreDeValue => $valorDeValue) { |
8 | $sqlDeValues .= ($primerValue === true ? "" : ",") . ":$nombreDeValue"; |
9 | $primerValue = false; |
10 | } |
11 | return $sqlDeValues; |
12 | } |
13 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/calculaArregloDeParametros.php"; |
4 | require_once __DIR__ . "/calculaSqlDeAsignaciones.php"; |
5 | |
6 | function delete(PDO $pdo, string $from, array $where) |
7 | { |
8 | $sql = "DELETE FROM $from"; |
9 | |
10 | if (sizeof($where) === 0) { |
11 | $pdo->exec($sql); |
12 | } else { |
13 | $sqlDeWhere = calculaSqlDeAsignaciones(" AND ", $where); |
14 | $sql .= " WHERE $sqlDeWhere"; |
15 | |
16 | $statement = $pdo->prepare($sql); |
17 | $parametros = calculaArregloDeParametros($where); |
18 | $statement->execute($parametros); |
19 | } |
20 | } |
21 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php"; |
4 | require_once __DIR__ . "/devuelveProblemDetails.php"; |
5 | require_once __DIR__ . "/devuelveProblemDetails.php"; |
6 | |
7 | function devuelveErrorInterno(Throwable $error) |
8 | { |
9 | devuelveProblemDetails(new ProblemDetails( |
10 | status: INTERNAL_SERVER_ERROR, |
11 | title: $error->getMessage(), |
12 | type: "/error/errorinterno.html" |
13 | )); |
14 | } |
15 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/devuelveResultadoNoJson.php"; |
4 | |
5 | function devuelveJson($resultado) |
6 | { |
7 | |
8 | $json = json_encode($resultado); |
9 | |
10 | if ($json === false) { |
11 | |
12 | devuelveResultadoNoJson(); |
13 | } else { |
14 | |
15 | http_response_code(200); |
16 | header("Content-Type: application/json"); |
17 | echo $json; |
18 | } |
19 | } |
20 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/devuelveResultadoNoJson.php"; |
4 | require_once __DIR__ . "/ProblemDetails.php"; |
5 | |
6 | function devuelveProblemDetails(ProblemDetails $details) |
7 | { |
8 | |
9 | $body = ["title" => $details->title]; |
10 | if ($details->type !== null) { |
11 | $body["type"] = $details->type; |
12 | } |
13 | if ($details->detail !== null) { |
14 | $body["detail"] = $details->detail; |
15 | } |
16 | |
17 | $json = json_encode($body); |
18 | |
19 | if ($json === false) { |
20 | |
21 | devuelveResultadoNoJson(); |
22 | } else { |
23 | |
24 | http_response_code($details->status); |
25 | header("Content-Type: application/problem+json"); |
26 | echo $json; |
27 | } |
28 | } |
29 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php"; |
4 | |
5 | function devuelveResultadoNoJson() |
6 | { |
7 | |
8 | http_response_code(INTERNAL_SERVER_ERROR); |
9 | header("Content-Type: application/problem+json"); |
10 | echo '{' . |
11 | '"title": "El resultado no puede representarse como JSON."' . |
12 | '"type": "/error/resultadonojson.html"' . |
13 | '}'; |
14 | } |
15 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/ProblemDetails.php"; |
4 | require_once __DIR__ . "/devuelveProblemDetails.php"; |
5 | require_once __DIR__ . "/devuelveErrorInterno.php"; |
6 | |
7 | function ejecutaServicio(callable $codigo) |
8 | { |
9 | try { |
10 | $codigo(); |
11 | } catch (ProblemDetails $details) { |
12 | devuelveProblemDetails($details); |
13 | } catch (Throwable $error) { |
14 | devuelveErrorInterno($error); |
15 | } |
16 | } |
17 |
1 | <?php |
2 | |
3 | function fetch( |
4 | PDOStatement|false $statement, |
5 | $parametros = [], |
6 | int $mode = PDO::FETCH_ASSOC, |
7 | $opcional = null |
8 | ) { |
9 | |
10 | if ($statement === false) { |
11 | |
12 | return false; |
13 | } else { |
14 | |
15 | if (sizeof($parametros) > 0) { |
16 | $statement->execute($parametros); |
17 | } |
18 | |
19 | if ($opcional === null) { |
20 | return $statement->fetch($mode); |
21 | } else { |
22 | $statement->setFetchMode($mode, $opcional); |
23 | return $statement->fetch(); |
24 | } |
25 | } |
26 | } |
27 |
1 | <?php |
2 | |
3 | function fetchAll( |
4 | PDOStatement|false $statement, |
5 | $parametros = [], |
6 | int $mode = PDO::FETCH_ASSOC, |
7 | $opcional = null |
8 | ): array { |
9 | |
10 | if ($statement === false) { |
11 | |
12 | return []; |
13 | } else { |
14 | |
15 | if (sizeof($parametros) > 0) { |
16 | $statement->execute($parametros); |
17 | } |
18 | |
19 | $resultado = $opcional === null |
20 | ? $statement->fetchAll($mode) |
21 | : $statement->fetchAll($mode, $opcional); |
22 | |
23 | if ($resultado === false) { |
24 | return []; |
25 | } else { |
26 | return $resultado; |
27 | } |
28 | } |
29 | } |
30 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/calculaSqlDeCamposDeInsert.php"; |
4 | require_once __DIR__ . "/calculaSqlDeValues.php"; |
5 | require_once __DIR__ . "/calculaArregloDeParametros.php"; |
6 | |
7 | function insert(PDO $pdo, string $into, array $values) |
8 | { |
9 | $sqlDeCampos = calculaSqlDeCamposDeInsert($values); |
10 | $sqlDeValues = calculaSqlDeValues($values); |
11 | $sql = "INSERT INTO $into ($sqlDeCampos) VALUES ($sqlDeValues)"; |
12 | $parametros = calculaArregloDeParametros($values); |
13 | $pdo->prepare($sql)->execute($parametros); |
14 | } |
15 |
1 | <?php |
2 | |
3 | const INTERNAL_SERVER_ERROR = 500; |
1 | <?php |
2 | |
3 | /** Detalle de los errores devueltos por un servicio. */ |
4 | class ProblemDetails extends Exception |
5 | { |
6 | |
7 | public int $status; |
8 | public string $title; |
9 | public ?string $type; |
10 | public ?string $detail; |
11 | |
12 | public function __construct( |
13 | int $status, |
14 | string $title, |
15 | ?string $type = null, |
16 | ?string $detail = null, |
17 | Throwable $previous = null |
18 | ) { |
19 | parent::__construct($title, $status, $previous); |
20 | $this->status = $status; |
21 | $this->type = $type; |
22 | $this->title = $title; |
23 | $this->detail = $detail; |
24 | } |
25 | } |
26 |
1 | <?php |
2 | |
3 | function recuperaJson() |
4 | { |
5 | return json_decode(file_get_contents("php://input")); |
6 | } |
7 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/fetchAll.php"; |
4 | require_once __DIR__ . "/calculaSqlDeAsignaciones.php"; |
5 | |
6 | function select( |
7 | PDO $pdo, |
8 | string $from, |
9 | array $where = [], |
10 | string $orderBy = "", |
11 | int $mode = PDO::FETCH_ASSOC, |
12 | $opcional = null |
13 | ) { |
14 | $sql = "SELECT * FROM $from"; |
15 | |
16 | if (sizeof($where) > 0) { |
17 | $sqlDeWhere = calculaSqlDeAsignaciones(" AND ", $where); |
18 | $sql .= " WHERE $sqlDeWhere"; |
19 | } |
20 | |
21 | if ($orderBy !== "") { |
22 | $sql .= " ORDER BY $orderBy"; |
23 | } |
24 | |
25 | if (sizeof($where) === 0) { |
26 | $statement = $pdo->query($sql); |
27 | return fetchAll($statement, [], $mode, $opcional); |
28 | } else { |
29 | $statement = $pdo->prepare($sql); |
30 | $parametros = calculaArregloDeParametros($where); |
31 | return fetchAll($statement, $parametros, $mode, $opcional); |
32 | } |
33 | } |
34 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/fetch.php"; |
4 | require_once __DIR__ . "/calculaArregloDeParametros.php"; |
5 | require_once __DIR__ . "/calculaSqlDeAsignaciones.php"; |
6 | |
7 | function selectFirst( |
8 | PDO $pdo, |
9 | string $from, |
10 | array $where = [], |
11 | string $orderBy = "", |
12 | int $mode = PDO::FETCH_ASSOC, |
13 | $opcional = null |
14 | ) { |
15 | $sql = "SELECT * FROM $from"; |
16 | |
17 | if (sizeof($where) > 0) { |
18 | $sqlDeWhere = calculaSqlDeAsignaciones(" AND ", $where); |
19 | $sql .= " WHERE $sqlDeWhere"; |
20 | } |
21 | |
22 | if ($orderBy !== "") { |
23 | $sql .= " ORDER BY $orderBy"; |
24 | } |
25 | |
26 | if (sizeof($where) === 0) { |
27 | $statement = $pdo->query($sql); |
28 | return fetch($statement, [], $mode, $opcional); |
29 | } else { |
30 | $statement = $pdo->prepare($sql); |
31 | $parametros = calculaArregloDeParametros($where); |
32 | return fetch($statement, $parametros, $mode, $opcional); |
33 | } |
34 | } |
35 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/calculaArregloDeParametros.php"; |
4 | require_once __DIR__ . "/calculaSqlDeAsignaciones.php"; |
5 | |
6 | |
7 | function update(PDO $pdo, string $table, array $set, array $where) |
8 | { |
9 | $sqlDeSet = calculaSqlDeAsignaciones(",", $set); |
10 | $sqlDeWhere = calculaSqlDeAsignaciones(" AND ", $where); |
11 | $sql = "UPDATE $table SET $sqlDeSet WHERE $sqlDeWhere"; |
12 | |
13 | $parametros = calculaArregloDeParametros($set); |
14 | foreach ($where as $nombreDeWhere => $valorDeWhere) { |
15 | $parametros[":$nombreDeWhere"] = $valorDeWhere; |
16 | } |
17 | $statement = $pdo->prepare($sql); |
18 | $statement->execute($parametros); |
19 | } |
20 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/BAD_REQUEST.php"; |
4 | require_once __DIR__ . "/ProblemDetails.php"; |
5 | |
6 | function validaJson($objeto) |
7 | { |
8 | |
9 | if ($objeto === null) |
10 | throw new ProblemDetails( |
11 | status: BAD_REQUEST, |
12 | title: "Los datos recibidos no son JSON.", |
13 | type: "/error/datosnojson.html", |
14 | detail: "Los datos recibidos no están en formato JSON.O", |
15 | ); |
16 | |
17 | return $objeto; |
18 | } |
19 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/BAD_REQUEST.php"; |
4 | require_once __DIR__ . "/ProblemDetails.php"; |
5 | |
6 | function validaNombre(false|string $nombre) |
7 | { |
8 | |
9 | if ($nombre === false) |
10 | throw new ProblemDetails( |
11 | status: BAD_REQUEST, |
12 | title: "Falta el nombre.", |
13 | type: "/error/faltanombre.html", |
14 | detail: "La solicitud no tiene el valor de nombre." |
15 | ); |
16 | |
17 | $trimNombre = trim($nombre); |
18 | |
19 | if ($trimNombre === "") |
20 | throw new ProblemDetails( |
21 | status: BAD_REQUEST, |
22 | title: "Nombre en blanco.", |
23 | type: "/error/nombreenblanco.html", |
24 | detail: "Pon texto en el campo nombre.", |
25 | ); |
26 | |
27 | return $trimNombre; |
28 | } |
29 |
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 son JSON</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>Los datos recibidos no son JSON</h1> |
16 | |
17 | <p> |
18 | Los datos recibidos no están en formato JSON. |
19 | </p> |
20 | |
21 | </body> |
22 | |
23 | </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>Falta el id</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>Falta el id</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>Falta el nombre</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>Falta el nombre</h1> |
16 | |
17 | <p>La solicitud no tiene el valor de nombre.</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</title> |
10 | |
11 | <body> |
12 | |
13 | <h1>El id debe ser texto</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>Nombre en blanco</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>Nombre en blanco</h1> |
16 | |
17 | <p>Pon texto en el campo nombre.</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 nombre debe ser texto</title> |
10 | |
11 | <body> |
12 | |
13 | <h1>El nombre debe ser texto</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> |
1 | AddType application/manifest+json .webmanifest |
2 | |
3 | ExpiresActive On |
4 | |
5 | Header set Cache-Control "max-age=1, must-revalidate" |
6 | |
7 | RewriteEngine On |
8 | |
9 | RewriteCond %{HTTP:X-Forwarded-Proto} !https |
10 | RewriteCond %{HTTPS} off |
11 | RewriteCond %{HTTP:CF-Visitor} !{"scheme":"https"} |
12 | RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] |
13 |
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 | ] |
13 | } |
14 | } |
En esta lección se muestra un ejemplo de sincronización de bases de datos.