12. Sincronización

Versión para imprimir.

A. Introducción

B. Diagrama entidad relación

Diagrama entidad relación

C. Diagrama de despliegue

Diagrama de despliegue

D. Hazlo funcionar

  1. Revisa el proyecto en Replit con la URl https://replit.com/@GilbertoPachec2/sincronizacion?v=1. Hazle fork al proyecto y córrelo. En el ambiente de desarrollo tienes la opción de descargar el proyecto en un zip.

  2. Usa o crea una cuenta de Google.

  3. Crea una cuenta de Replit usando la cuenta de Google.

  4. Crea un proyecto PHP Web Server en Replit y edita o sube los archivos de este proyecto.

  5. Depura el proyecto.

  6. Crea la cover page o página de spotlight del proyecto.

E. Archivos

Haz clic en los triángulos para expandir las carpetas

F. index.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>Sincronizacion</title>
10
11 <script type="module" src="js/configura.js"></script>
12
13</head>
14
15<body>
16
17 <h1>Sincronizacion</h1>
18
19 <p><a href="agrega.html">Agregar</a></p>
20
21 <ul id="lista">
22 <li><progress max="100">Cargando…</progress></li>
23 </ul>
24
25 <script type="module">
26
27 import { muestraError } from "./lib/js/muestraError.js"
28 import { enviaJson } from "./lib/js/enviaJson.js"
29 import { Pasatiempo } from "./js/modelo/Pasatiempo.js"
30 import {
31 validaQueTengaLasPropiedadesDePasatiempo
32 } from "./js/modelo/validaQueTengaLasPropiedadesDePasatiempo.js"
33 import {
34 pasatiempoConsultaNoEliminados
35 } from "./js/bd/pasatiempoConsultaNoEliminados.js"
36 import { pasatiempoConsultaTodos } from "./js/bd/pasatiempoConsultaTodos.js"
37 import { pasatiemposReemplaza } from "./js/bd/pasatiemposReemplaza.js"
38
39 /**
40 * Cada 20 segundos (2000 milisegundos) después de la última
41 * sincronización, los datos se envían al servidor para volver a
42 * sincronizarse con los datos del servidor.
43 */
44 const MILISEGUNDOS_PARA_VOLVER_A_SINCRONIZAR = 20000
45 let noHaSincronizado = true
46
47 // Crea y pone en funcionamiento el worker del archivo "render.js".
48 const worker = new Worker("render.js", { type: "module" })
49
50 // Se invoca cuando el worker envía un mensaje a la página.
51 worker.onmessage = async event => {
52 lista.innerHTML = event.data
53 if (noHaSincronizado) {
54 noHaSincronizado = false
55 await sincroniza()
56 } else {
57 esperaUnPocoYLanzaSincronizacion()
58 }
59 }
60
61 pasatiempoConsultaNoEliminados()
62 .then(renderEnWebWorker)
63 .catch(muestraError)
64
65 function renderEnWebWorker(datos) {
66 if (!Array.isArray(datos))
67 throw new Error("Los datos no están en un arreglo.")
68 for (const dato of datos) {
69 validaQueTengaLasPropiedadesDePasatiempo(dato)
70 }
71 // Manda los datos al worker para renderizar.
72 worker.postMessage(datos)
73 }
74
75 async function sincroniza() {
76 try {
77 if (navigator.onLine) {
78 const todos = await pasatiempoConsultaTodos()
79 const respuesta = await enviaJson("srv/srvSincro.php", todos)
80 const datosNuevos = respuesta.body
81 renderEnWebWorker(datosNuevos)
82 await pasatiemposReemplaza(datosNuevos)
83 } else {
84 esperaUnPocoYLanzaSincronizacion()
85 }
86 } catch (error) {
87 muestraError(error)
88 esperaUnPocoYLanzaSincronizacion()
89 }
90 }
91
92 function esperaUnPocoYLanzaSincronizacion() {
93 setTimeout(sincroniza, MILISEGUNDOS_PARA_VOLVER_A_SINCRONIZAR)
94 }
95
96 </script>
97
98</body>
99
100</html>

G. render.js

1/* Ejemplo de render en el cliente. No se usa import
2 * porque Firefox no lo soporta en los web workers. */
3
4// Verifica si el código corre dentro de un web worker.
5if (self instanceof WorkerGlobalScope) {
6
7 // Se invoca al recibir un mensaje de la página.
8 onmessage = event => {
9
10 /**
11 * @type { {
12 * uuid: string,
13 * nombre: string,
14 * modificacion: number,
15 * eliminado: boolean,
16 * } [] }
17 */
18 const pasatiempos = event.data
19
20 let render = ""
21 for (const modelo of pasatiempos) {
22 const nombre = htmlentities(modelo.nombre)
23 const searchParams = new URLSearchParams([["uuid", modelo.uuid]])
24 const params = htmlentities(searchParams.toString())
25 render += /* html */
26 `<li>
27 <p><a href="modifica.html?${params}">${nombre}</a></p>
28 </li>`
29 }
30
31 // Envía el render a la página que invocó este web worker.
32 self.postMessage(render)
33
34 }
35
36}
37
38/**
39 * Codifica un texto para que cambie los caracteres
40 * especiales y no se pueda interpretar como
41 * etiiqueta HTML. Esta técnica evita la inyección
42 * de código.
43 * @param { string } texto
44 * @returns { string } un texto que no puede
45 * interpretarse como HTML. */
46export function htmlentities(texto) {
47 return texto.replace(/[<>"']/g, textoDetectado => {
48 switch (textoDetectado) {
49 case "<": return "<"
50 case ">": return ">"
51 case '"': return """
52 case "'": return "'"
53 default: return textoDetectado
54 }
55 })
56}

H. agrega.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/modelo/Pasatiempo.js"></script>
14 <script type="module" src="js/bd/pasatiempoAgrega.js"></script>
15
16</head>
17
18<body>
19
20 <form id="forma" onsubmit="
21 event.preventDefault()
22 // Lee el nombre, quitándole los espacios al inicio y al final.
23 const nombre = forma.nombre.value.trim()
24 const modelo = new Pasatiempo({ nombre })
25 pasatiempoAgrega(modelo)
26 .then(json => location.href = 'index.html')
27 .catch(muestraError)">
28
29 <h1>Agregar</h1>
30
31 <p><a href="index.html">Cancelar</a></p>
32
33 <p>
34 <label>
35 Nombre *
36 <input name="nombre">
37 </label>
38 </p>
39 <p>* Obligatorio</p>
40 <p><button type="submit">Agregar</button></p>
41
42 </form>
43
44</body>
45
46</html>

I. modifica.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="lib/js/confirmaEliminar.js"></script>
15 <script type="module" src="js/modelo/Pasatiempo.js"></script>
16 <script type="module" src="js/bd/pasatiempoBusca.js"></script>
17 <script type="module" src="js/bd/pasatiempoElimina.js"></script>
18 <script type="module" src="js/bd/pasatiempoModifica.js"></script>
19
20 <script>
21
22 // Obtiene los parámetros de la página.
23 const parametros = new URL(location.href).searchParams
24
25 const uuid = parametros.get("uuid")
26
27 </script>
28
29</head>
30
31<body onload="if (uuid !== null) {
32 pasatiempoBusca(uuid)
33 .then(async pasatiempo => {
34 if (pasatiempo === undefined) throw new Error('Pasatiempo no encontrado.')
35 await muestraObjeto(forma, { nombre: { value: pasatiempo.nombre } })
36 })
37 .catch(muestraError)
38 }">
39
40 <form id="forma" onsubmit="
41 event.preventDefault()
42 if (uuid !== null) {
43 // Lee el nombre, quitándole los espacios al inicio y al final.
44 const nombre = forma.nombre.value.trim()
45 const modelo = new Pasatiempo({ nombre, uuid })
46 pasatiempoModifica(modelo)
47 .then(json => location.href = 'index.html')
48 .catch(muestraError)
49 }">
50
51 <h1>Modificar</h1>
52
53 <p><a href="index.html">Cancelar</a></p>
54
55 <p>
56 <label>
57 Nombre *
58 <input name="nombre" value="Cargando…">
59 </label>
60 </p>
61
62 <p>* Obligatorio</p>
63
64 <p>
65
66 <button type="submit">Guardar</button>
67
68 <button type="button" onclick="if (uuid !== null && confirmaEliminar()) {
69 pasatiempoElimina(uuid)
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>

J. instruccionesListadoSw.txt

1Generar el listado de archivos del sw.js desde Visual Studio Code.
21. Abrir una terminal desde el menú
3 Terminal > New Terminal
4
52. Desde la terminal introducir la orden:
6 Get-ChildItem -path . -Recurse | Select Directory,Name | Out-File archivos.txt
7
83. Abrir el archivo generado, que se llama
9 archivos.txt
10 y sobre este, realizar los pasos que siguen:
11
124. Quita del archivo archivos.txt:
13 * el encabezado,
14 * todas las carpetas,
15 * el archivo .vscode/settings.json,
16 * el archivo archivos.txt,
17 * este archivo (instruccionesListadoSw.txt),
18 * el archivo jsconfig.json,
19 * el archivo sw.js,
20 * el archivo de la base de datos, que termina en ".bd" y
21 está en la carpeta srv,
22 * todos los archivos de php y
23 * las líneas en blanco del final
24
255. Cambia los \ por / desde Visual Studio Code con las siguientes
26 combinaciones de teclas:
27
28 Ctrl+H En el diálogo que aparece introduce lo siguiente:
29 Find:\
30 Replace:/
31
32 Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
33
346. Coloca las comillas y coma del final de cada línea desde Visual
35 Studio Code con las siguientes combinaciones de teclas:
36
37 Ctrl+H En el diálogo que aparece, selecciona el botón
38 ".*"
39 e introduce lo siguiente:
40 Find:\s*$
41 Replace:",
42
43 Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
44
457. Marca la carpeta inicial, presiona la combinación de teclas:
46
47 Shift+Ctrl+L
48
49 borra la selección, teclea " y luego ESC
50
518. Cambia las secuencias de espacios por / con las siguientes
52 combinaciones de teclas:
53
54 Ctrl+H En el diálogo que aparece, selecciona el botón
55 ".*"
56 e introduce lo siguiente:
57 Find:\s+
58 Replace:/
59
60 Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
61
629. Cambia las "/ por " con las siguientes combinaciones de teclas:
63
64 Ctrl+H En el diálogo que aparece, quita la selección del botón
65 ".*"
66 e introduce lo siguiente:
67 Find:"/
68 Replace:"
69
70 Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
71
7210. Copia el texto al archivo
73 sw.js
74 en el contenido del arreglo llamado ARCHIVOS, pero recuerda
75 mantener el último elemento, que dice:
76 "/"

K. archivos.txt

1"agrega.html",
2"index.html",
3"modifica.html",
4"render.js",
5"error/eliminadoincorrecto.html",
6"error/errorinterno.html",
7"error/modificacionincorrecta.html",
8"error/nojson.html",
9"error/nombreincorrecto.html",
10"error/uuidincorrecto.html",
11"js/configura.js",
12"js/bd/Bd.js",
13"js/bd/pasatiempoAgrega.js",
14"js/bd/pasatiempoBusca.js",
15"js/bd/pasatiempoConsultaNoEliminados.js",
16"js/bd/pasatiempoConsultaTodos.js",
17"js/bd/pasatiempoElimina.js",
18"js/bd/pasatiempoModifica.js",
19"js/bd/pasatiemposReemplaza.js",
20"js/modelo/leePasatiempo.js",
21"js/modelo/Pasatiempo.js",
22"js/modelo/validaQueTengaLasPropiedadesDePasatiempo.js",
23"lib/js/bdConsulta.js",
24"lib/js/bdEjecuta.js",
25"lib/js/confirmaEliminar.js",
26"lib/js/enviaJson.js",
27"lib/js/invocaServicio.js",
28"lib/js/JsonResponse.js",
29"lib/js/muestraError.js",
30"lib/js/muestraObjeto.js",
31"lib/js/ProblemDetails.js",
32"lib/js/registraServiceWorkerSiEsSoportado.js",

L. sw.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 */
18const VERSION = "1.00"
19
20/**
21 * Nombre de la carpeta de caché.
22 */
23const CACHE = "sincro"
24
25/**
26 * Archivos requeridos para que la aplicación funcione fuera de línea.
27 */
28const ARCHIVOS = [
29 "agrega.html",
30 "index.html",
31 "modifica.html",
32 "render.js",
33 "error/eliminadoincorrecto.html",
34 "error/errorinterno.html",
35 "error/modificacionincorrecta.html",
36 "error/nojson.html",
37 "error/nombreincorrecto.html",
38 "error/uuidincorrecto.html",
39 "js/configura.js",
40 "js/bd/Bd.js",
41 "js/bd/pasatiempoAgrega.js",
42 "js/bd/pasatiempoBusca.js",
43 "js/bd/pasatiempoConsultaNoEliminados.js",
44 "js/bd/pasatiempoConsultaTodos.js",
45 "js/bd/pasatiempoElimina.js",
46 "js/bd/pasatiempoModifica.js",
47 "js/bd/pasatiemposReemplaza.js",
48 "js/modelo/leePasatiempo.js",
49 "js/modelo/Pasatiempo.js",
50 "js/modelo/validaQueTengaLasPropiedadesDePasatiempo.js",
51 "lib/js/bdConsulta.js",
52 "lib/js/bdEjecuta.js",
53 "lib/js/confirmaEliminar.js",
54 "lib/js/enviaJson.js",
55 "lib/js/invocaServicio.js",
56 "lib/js/JsonResponse.js",
57 "lib/js/muestraError.js",
58 "lib/js/muestraObjeto.js",
59 "lib/js/ProblemDetails.js",
60 "lib/js/registraServiceWorkerSiEsSoportado.js",
61 "/"
62]
63
64// Verifica si el código corre dentro de un service worker.
65if (self instanceof ServiceWorkerGlobalScope) {
66 // Evento al empezar a instalar el servide worker,
67 self.addEventListener("install",
68 (/** @type {ExtendableEvent} */ evt) => {
69 console.log("El service worker se está instalando.")
70 evt.waitUntil(llenaElCache())
71 })
72
73 // Evento al solicitar información a la red.
74 self.addEventListener("fetch", (/** @type {FetchEvent} */ evt) => {
75 if (evt.request.method === "GET") {
76 evt.respondWith(buscaLaRespuestaEnElCache(evt))
77 }
78 })
79
80 // Evento cuando el service worker se vuelve activo.
81 self.addEventListener("activate",
82 () => console.log("El service worker está activo."))
83}
84
85async function llenaElCache() {
86 console.log("Intentando cargar caché:", CACHE, ".")
87 // Borra todos los cachés.
88 const keys = await caches.keys()
89 for (const key of keys) {
90 await caches.delete(key)
91 }
92 // Abre el caché de este service worker.
93 const cache = await caches.open(CACHE)
94 // Carga el listado de ARCHIVOS.
95 await cache.addAll(ARCHIVOS)
96 console.log("Cache cargado:", CACHE, ".")
97 console.log("Versión:", VERSION, ".")
98}
99
100/** @param {FetchEvent} evt */
101async function buscaLaRespuestaEnElCache(evt) {
102 // Abre el caché.
103 const cache = await caches.open(CACHE)
104 const request = evt.request
105 /* Busca la respuesta a la solicitud en el contenido del caché, sin
106 * tomar en cuenta la parte después del símbolo "?" en la URL. */
107 const response = await cache.match(request, { ignoreSearch: true })
108 if (response === undefined) {
109 /* Si no la encuentra, empieza a descargar de la red y devuelve
110 * la promesa. */
111 return fetch(request)
112 } else {
113 // Si la encuentra, devuelve la respuesta encontrada en el caché.
114 return response
115 }
116}

M. Carpeta « js »

Versión para imprimir.

A. js / configura.js

1import {
2 registraServiceWorkerSiEsSoportado
3} from "../lib/js/registraServiceWorkerSiEsSoportado.js"
4
5registraServiceWorkerSiEsSoportado("sw.js")

B. Carpeta « js / bd »

1. js / bd / Bd.js

1export const NOMBRE_DEL_ALMACEN_PASATIEMPO = "Pasatiempo"
2export const NOMBRE_DEL_INDICE_NOMBRE = "nombre"
3const BD_NOMBRE = "sincronizacion"
4const BD_VERSION = 1
5
6/** @type { Promise<IDBDatabase> } */
7export const Bd = new Promise((resolve, reject) => {
8
9 /* Se solicita abrir la base de datos, indicando nombre y
10 * número de versión. */
11 const solicitud = indexedDB.open(BD_NOMBRE, BD_VERSION)
12
13 // Si se presenta un error, rechaza la promesa.
14 solicitud.onerror = () => reject(solicitud.error)
15
16 // Si se abre con éxito, devuelve una conexión a la base de datos.
17 solicitud.onsuccess = () => resolve(solicitud.result)
18
19 // Si es necesario, se inicia una transacción para cambio de versión.
20 solicitud.onupgradeneeded = () => {
21
22 const bd = solicitud.result
23
24 // Como hay cambio de versión, borra el almacén si es que existe.
25 if (bd.objectStoreNames.contains(NOMBRE_DEL_ALMACEN_PASATIEMPO)) {
26 bd.deleteObjectStore(NOMBRE_DEL_ALMACEN_PASATIEMPO)
27 }
28
29 // Crea el almacén "Pasatiempo" con el campo llave "uuid".
30 const almacenPasatiempo =
31 bd.createObjectStore(NOMBRE_DEL_ALMACEN_PASATIEMPO, { keyPath: "uuid" })
32
33 // Crea un índice ordenado por el campo "nombre" que no acepta duplicados.
34 almacenPasatiempo.createIndex(NOMBRE_DEL_INDICE_NOMBRE, "nombre")
35 }
36
37})

2. js / bd / pasatiempoAgrega.js

1import { bdEjecuta } from "../../lib/js/bdEjecuta.js"
2import { Pasatiempo } from "../modelo/Pasatiempo.js"
3import { Bd, NOMBRE_DEL_ALMACEN_PASATIEMPO } from "./Bd.js"
4
5let secuencia = 0
6
7/**
8 * @param { Pasatiempo } modelo
9 */
10export async function pasatiempoAgrega(modelo) {
11 modelo.validaNuevo()
12 modelo.modificacion = Date.now()
13 modelo.eliminado = false
14
15 // Genera uuid único en internet.
16 modelo.uuid = Date.now().toString() + Math.random() + secuencia
17 secuencia++
18
19 return bdEjecuta(Bd, [NOMBRE_DEL_ALMACEN_PASATIEMPO],
20 transaccion => {
21 const almacenPasatiempo =
22 transaccion.objectStore(NOMBRE_DEL_ALMACEN_PASATIEMPO)
23 almacenPasatiempo.add(modelo)
24 })
25}
26
27// Permite que los eventos de html usen la función.
28window["pasatiempoAgrega"] = pasatiempoAgrega

3. js / bd / pasatiempoBusca.js

1import { bdConsulta } from "../../lib/js/bdConsulta.js"
2import { leePasatiempo } from "../modelo/leePasatiempo.js"
3import { Pasatiempo } from "../modelo/Pasatiempo.js"
4import { Bd, NOMBRE_DEL_ALMACEN_PASATIEMPO } from "./Bd.js"
5
6/**
7 * @param { string | null } uuid
8 */
9export async function pasatiempoBusca(uuid) {
10
11 if (uuid === null)
12 throw new Error("Falta el uuid")
13
14 return bdConsulta(Bd, [NOMBRE_DEL_ALMACEN_PASATIEMPO],
15 /**
16 * @param { IDBTransaction } transaccion
17 * @param { (resultado: Pasatiempo|undefined) => any } resolve
18 */
19 (transaccion, resolve) => {
20
21 /* Pide el primer registro de almacén pasatiempo que tenga como
22 * llave primaria el valor del parámetro uuid. */
23 const consulta =
24 transaccion.objectStore(NOMBRE_DEL_ALMACEN_PASATIEMPO).get(uuid)
25
26 /* onsuccess se invoca solo una vez, devolviendo el registro
27 * solicitado. */
28 consulta.onsuccess = () => {
29
30 /* Se recupera el registro solicitado usando
31 * consulta.result
32 * Si el registro no se encuentra se recupera undefined. */
33 const objeto = consulta.result
34
35 if (objeto !== undefined) {
36 const modelo = leePasatiempo(objeto)
37 if (!modelo.eliminado) {
38 resolve(modelo)
39 }
40 }
41
42 resolve(undefined)
43
44 }
45
46 })
47
48}
49
50// Permite que los eventos de html usen la función.
51window["pasatiempoBusca"] = pasatiempoBusca

4. js / bd / pasatiempoConsultaNoEliminados.js

1import { bdConsulta } from "../../lib/js/bdConsulta.js"
2import {
3 Bd,
4 NOMBRE_DEL_ALMACEN_PASATIEMPO, NOMBRE_DEL_INDICE_NOMBRE
5} from "./Bd.js"
6
7export async function pasatiempoConsultaNoEliminados() {
8
9 return bdConsulta(Bd, [NOMBRE_DEL_ALMACEN_PASATIEMPO],
10 /**
11 * @param { IDBTransaction } transaccion
12 * @param { (resultado: any[]) => any } resolve
13 */
14 (transaccion, resolve) => {
15
16 const resultado = []
17
18 const almacenPasatiempo =
19 transaccion.objectStore(NOMBRE_DEL_ALMACEN_PASATIEMPO)
20
21 // Usa el índice "nombre" para reculeprar los datos ordenadors.
22 const indiceNombre = almacenPasatiempo.index(NOMBRE_DEL_INDICE_NOMBRE)
23
24 /* Pide un cursor para recorrer cada uno de los registristros
25 * que devuelve la consulta. */
26 const consulta = indiceNombre.openCursor()
27
28 /* onsuccess se invoca por cada uno de los registros de la
29 * consulta y una vez cuando se acaban dichos registros. */
30 consulta.onsuccess = () => {
31
32 /* El cursor que corresponde al registro se recupera usando
33 * consulta.result */
34 const cursor = consulta.result
35
36 if (cursor === null) {
37
38 /* Si el cursor es igual a null, ya no hay más registros que
39 * procesar; por lo mismo, se devuelve el resultado con los
40 * pasatiempos recuperados, usando
41 * resolve(resultado). */
42 resolve(resultado)
43
44 } else {
45
46 /* Si el cursor es diferente a null, si hay más registros.
47 * El registro que sigue se obtiene con
48 * cursor.value */
49 const modelo = cursor.value
50 if (!modelo.eliminado) {
51 resultado.push(modelo)
52 }
53
54 /* Busca el siguiente registro de la consulta, que se obtiene
55 * la siguiente vez que se invoque la función
56 * onsuccess. */
57 cursor.continue()
58
59 }
60
61 }
62
63 })
64
65}
66
67// Permite que los eventos de html usen la función.
68window["pasatiempoConsultaNoEliminados"] = pasatiempoConsultaNoEliminados

5. js / bd / pasatiempoConsultaTodos.js

1import { bdConsulta } from "../../lib/js/bdConsulta.js"
2import { NOMBRE_DEL_ALMACEN_PASATIEMPO, Bd } from "./Bd.js"
3
4/**
5 * Lista todos los objetos, incluyendo los que tienen
6 * borrado lógico.
7 */
8export async function pasatiempoConsultaTodos() {
9
10 return bdConsulta(Bd, [NOMBRE_DEL_ALMACEN_PASATIEMPO],
11 /**
12 * @param { IDBTransaction } transaccion
13 * @param { (result: any[]) => any } resolve
14 */
15 (transaccion, resolve) => {
16
17 const resultado = []
18
19 /* Pide un cursor para recorrer cada uno de los
20 * registristros que devuelve la consulta. */
21 const consulta =
22 transaccion.objectStore(NOMBRE_DEL_ALMACEN_PASATIEMPO).openCursor()
23
24 /* onsuccess se invoca por cada uno de los registros de la
25 * consulta y una vez cuando se acaban dichos registros. */
26 consulta.onsuccess = () => {
27
28 /* El cursor que corresponde al registro se recupera usando
29 * consulta.result */
30 const cursor = consulta.result
31
32 if (cursor === null) {
33
34 /* Si el cursor es igual a null, ya no hay más registros que
35 * procesar; por lo mismo, se devuelve el resultado con los
36 * pasatiempos recuperados, usando
37 * resolve(resultado). */
38 resolve(resultado)
39
40 } else {
41
42 /* Si el cursor es diferente a null, si hay más registros.
43 * El registro que sigue se obtiene con
44 * cursor.value*/
45 resultado.push(cursor.value)
46
47 /* Busca el siguiente registro de la consulta, que se obtiene
48 * la siguiente vez que se invoque la función
49 * onsuccess. */
50 cursor.continue()
51
52 }
53
54 }
55
56 })
57
58}

6. js / bd / pasatiempoElimina.js

1import { bdEjecuta } from "../../lib/js/bdEjecuta.js"
2import { NOMBRE_DEL_ALMACEN_PASATIEMPO, Bd } from "./Bd.js"
3import { pasatiempoBusca } from "./pasatiempoBusca.js"
4
5/**
6 * @param { string } uuid
7 */
8export async function pasatiempoElimina(uuid) {
9 const modelo = await pasatiempoBusca(uuid)
10 if (modelo !== undefined) {
11 modelo.modificacion = Date.now()
12 modelo.eliminado = true
13 return bdEjecuta(Bd, [NOMBRE_DEL_ALMACEN_PASATIEMPO],
14 transaccion => {
15 const almacenPasatiempo =
16 transaccion.objectStore(NOMBRE_DEL_ALMACEN_PASATIEMPO)
17 almacenPasatiempo.put(modelo)
18 })
19 }
20}
21
22// Permite que los eventos de html usen la función.
23window["pasatiempoElimina"] = pasatiempoElimina

7. js / bd / pasatiempoModifica.js

1import { bdEjecuta } from "../../lib/js/bdEjecuta.js"
2import { Pasatiempo } from "../modelo/Pasatiempo.js"
3import { Bd, NOMBRE_DEL_ALMACEN_PASATIEMPO } from "./Bd.js"
4import { pasatiempoBusca } from "./pasatiempoBusca.js"
5
6/**
7 * @param { Pasatiempo } modelo
8 */
9export async function pasatiempoModifica(modelo) {
10 modelo.valida()
11 const anterior = await pasatiempoBusca(modelo.uuid)
12 if (anterior === undefined) {
13 return undefined
14 } else {
15 modelo.modificacion = Date.now()
16 modelo.eliminado = false
17 return bdEjecuta(Bd, [NOMBRE_DEL_ALMACEN_PASATIEMPO],
18 transaccion => {
19 const almacenPasatiempo =
20 transaccion.objectStore(NOMBRE_DEL_ALMACEN_PASATIEMPO)
21 almacenPasatiempo.put(modelo)
22 })
23 }
24}
25
26// Permite que los eventos de html usen la función.
27window["pasatiempoModifica"] = pasatiempoModifica

8. js / bd / pasatiemposReemplaza.js

1import { bdEjecuta } from "../../lib/js/bdEjecuta.js"
2import { Bd, NOMBRE_DEL_ALMACEN_PASATIEMPO } from "./Bd.js"
3
4/**
5 * Borra el contenido del almacén pasatiempo y guarda el
6 * contenido del listado.
7 * @param {any[]} datosNuevos
8 */
9export async function pasatiemposReemplaza(datosNuevos) {
10 return bdEjecuta(Bd, [NOMBRE_DEL_ALMACEN_PASATIEMPO],
11 transaccion => {
12 const almacenPasatiempo =
13 transaccion.objectStore(NOMBRE_DEL_ALMACEN_PASATIEMPO)
14 almacenPasatiempo.clear()
15 for (const objeto of datosNuevos) {
16 almacenPasatiempo.add(objeto)
17 }
18 })
19}

C. Carpeta « js / modelo »

1. js / modelo / leePasatiempo.js

1import { Pasatiempo } from "./Pasatiempo.js"
2import {
3 validaQueTengaLasPropiedadesDePasatiempo
4} from "./validaQueTengaLasPropiedadesDePasatiempo.js"
5
6/** @param { any } objeto */
7export function leePasatiempo(objeto) {
8 const validado = validaQueTengaLasPropiedadesDePasatiempo(objeto)
9 return new Pasatiempo({
10 uuid: validado.uuid,
11 nombre: validado.nombre,
12 modificacion: validado.modificacion,
13 eliminado: validado.eliminado,
14 })
15}

2. js / modelo / Pasatiempo.js

1export class Pasatiempo {
2
3 /**
4 * @param { {
5 * uuid?: string,
6 * nombre?: string,
7 * modificacion?: number,
8 * eliminado?: boolean,
9 * } } modelo
10 */
11 constructor(modelo) {
12 this.nombre = modelo.nombre === undefined
13 ? ""
14 : modelo.nombre
15 this.uuid = modelo.uuid === undefined
16 ? ""
17 : modelo.uuid
18 this.modificacion = modelo.modificacion === undefined
19 ? NaN
20 : modelo.modificacion
21 this.eliminado = modelo.eliminado === undefined
22 ? true
23 : modelo.eliminado
24 }
25
26 validaNuevo() {
27 if (this.nombre === "")
28 throw new Error("Falta el nombre.")
29 }
30
31 valida() {
32 if (this.uuid === "")
33 throw new Error("Falta el uuid.")
34 if (this.nombre === "")
35 throw new Error("Falta el nombre.")
36 }
37
38}
39
40// Permite que los eventos de html usen la clase.
41window["Pasatiempo"] = Pasatiempo

3. js / modelo / validaQueTengaLasPropiedadesDePasatiempo.js

1/**
2 * @param { any } objeto
3 * @returns { {
4 * uuid: string,
5 * nombre: string,
6 * modificacion: number,
7 * eliminado: boolean,
8 * } }
9 */
10export function validaQueTengaLasPropiedadesDePasatiempo(objeto) {
11
12 if (typeof objeto.uuid !== "string")
13 throw new Error("El uuid debe ser texto.")
14
15 if (typeof objeto.nombre !== "string")
16 throw new Error("El nombre debe ser texto.")
17
18 if (typeof objeto.modificacion !== "number")
19 throw new Error("El campo modificacion debe ser número.")
20
21 if (typeof objeto.eliminado !== "boolean")
22 throw new Error("El campo eliminado debe ser booleano.")
23
24 return objeto
25
26}

N. Carpeta « srv »

Versión para imprimir.

A. srv / srvSincro.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/leeJson.php";
5require_once __DIR__ . "/modelo/Pasatiempo.php";
6require_once __DIR__ . "/modelo/leePasatiempo.php";
7require_once __DIR__ . "/bd/pasatiempoAgrega.php";
8require_once __DIR__ . "/bd/pasatiempoBusca.php";
9require_once __DIR__ . "/bd/pasatiempoConsultaNoEliminados.php";
10require_once __DIR__ . "/bd/pasatiempoModifica.php";
11
12ejecutaServicio(function () {
13 $lista = leeJson();
14 if (!is_array($lista)) {
15 $lista = [];
16 }
17 foreach ($lista as $objeto) {
18 $modeloEnElCliente = leePasatiempo($objeto);
19 $modeloEnElServidor = pasatiempoBusca($modeloEnElCliente->uuid);
20 if ($modeloEnElServidor === false) {
21 /* CONFLICTO. El objeto no ha estado en el servidor.
22 * AGREGARLO solamente si no está eliminado. */
23 if (!$modeloEnElCliente->eliminado) {
24 pasatiempoAgrega($modeloEnElCliente);
25 }
26 } elseif (
27 !$modeloEnElServidor->eliminado && $modeloEnElCliente->eliminado
28 ) {
29 /* CONFLICTO. El registro está en el servidor, donde no se ha
30 * eliminado, pero ha sido eliminado en el cliente.
31 * Gana el cliente, porque optamos por no revivir lo que se ha
32 * borrado. */
33 pasatiempoModifica($modeloEnElCliente);
34 } else if (
35 !$modeloEnElCliente->eliminado && !$modeloEnElServidor->eliminado
36 ) {
37 /* CONFLICTO. El registro está tanto en el servidor como en el
38 * cliente. Los datos pueden ser diferentes.
39 * PREVALECE LA FECHA MÁS GRANDE. Cuando gana el servidor no se
40 * hace nada.*/
41 if (
42 $modeloEnElCliente->modificacion > $modeloEnElServidor->modificacion
43 ) {
44 // La versión del cliente es más nueva y prevalece.
45 pasatiempoModifica($modeloEnElCliente);
46 }
47 }
48 }
49 $lista = pasatiempoConsultaNoEliminados();
50 return $lista;
51});
52

B. Carpeta « srv / bd »

1. srv / bd / Bd.php

1<?php
2
3require_once __DIR__ . "/../modelo/Pasatiempo.php";
4require_once __DIR__ . "/bdCrea.php";
5
6class Bd
7{
8
9 private static ?PDO $conexion = null;
10
11 static function getConexion(): PDO
12 {
13 if (self::$conexion === null) {
14 self::$conexion = new PDO(
15 // cadena de conexión
16 "sqlite:sincronizacion.db",
17 // usuario
18 null,
19 // contraseña
20 null,
21 // Opciones: conexiones persistentes y lanza excepciones.
22 [PDO::ATTR_PERSISTENT => true, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
23 );
24
25 bdCrea(self::$conexion);
26 }
27
28 return self::$conexion;
29 }
30}
31

2. srv / bd / bdCrea.php

1<?php
2
3function bdCrea(PDO $con)
4{
5 $con->exec(
6 'CREATE TABLE IF NOT EXISTS PASATIEMPO (
7 PAS_UUID TEXT NOT NULL,
8 PAS_NOMBRE TEXT NOT NULL,
9 PAS_MODIFICACION INTEGER NOT NULL,
10 PAS_ELIMINADO INTEGER NOT NULL,
11 CONSTRAINT PAS_PK
12 PRIMARY KEY(PAS_UUID)
13 )'
14 );
15}
16

3. srv / bd / pasatiempoAgrega.php

1<?php
2
3require_once __DIR__ . "/../modelo/Pasatiempo.php";
4require_once __DIR__ . "/Bd.php";
5
6function pasatiempoAgrega(Pasatiempo $modelo) {
7 $modelo->valida();
8 $con = Bd::getConexion();
9 $stmt = $con->prepare(
10 "INSERT INTO PASATIEMPO
11 (PAS_UUID, PAS_NOMBRE, PAS_MODIFICACION, PAS_ELIMINADO)
12 VALUES
13 (:uuid, :nombre, :modificacion, :eliminado)"
14 );
15 $stmt->execute([
16 ":uuid" => $modelo->uuid,
17 ":nombre" => $modelo->nombre,
18 ":modificacion" => $modelo->modificacion,
19 ":eliminado" => $modelo->eliminado ? 1 : 0
20 ]);
21}
22

4. srv / bd / pasatiempoBusca.php

1<?php
2
3require_once __DIR__ . "/../modelo/Pasatiempo.php";
4require_once __DIR__ . "/Bd.php";
5
6function pasatiempoBusca(string $uuid): false|Pasatiempo
7{
8 $con = Bd::getConexion();
9 $stmt = $con->prepare(
10 "SELECT
11 PAS_UUID AS uuid,
12 PAS_NOMBRE AS nombre,
13 PAS_MODIFICACION AS modificacion,
14 PAS_ELIMINADO AS eliminado
15 FROM PASATIEMPO
16 WHERE PAS_UUID = :uuid"
17 );
18 $stmt->execute([":uuid" => $uuid]);
19 $stmt->setFetchMode(
20 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
21 Pasatiempo::class
22 );
23 return $stmt->fetch();
24}
25

5. srv / bd / pasatiempoConsultaNoEliminados.php

1<?php
2
3require_once __DIR__ . "/../modelo/Pasatiempo.php";
4require_once __DIR__ . "/Bd.php";
5require_once __DIR__ . "/../../lib/php/recibeFetchAll.php";
6
7/** @return Pasatiempo[] */
8function pasatiempoConsultaNoEliminados()
9{
10 $con = Bd::getConexion();
11 $stmt = $con->query(
12 "SELECT
13 PAS_UUID AS uuid,
14 PAS_NOMBRE AS nombre,
15 PAS_MODIFICACION AS modificacion,
16 PAS_ELIMINADO AS eliminado
17 FROM PASATIEMPO
18 WHERE PAS_ELIMINADO = 0
19 ORDER BY PAS_NOMBRE"
20 );
21 $resultado = $stmt->fetchAll(
22 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
23 Pasatiempo::class
24 );
25 return recibeFetchAll($resultado);
26}
27

6. srv / bd / pasatiempoModifica.php

1<?php
2
3require_once __DIR__ . "/../modelo/Pasatiempo.php";
4require_once __DIR__ . "/Bd.php";
5
6function pasatiempoModifica(Pasatiempo $modelo)
7{
8 $modelo->valida();
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "UPDATE PASATIEMPO
12 SET
13 PAS_NOMBRE = :nombre,
14 PAS_MODIFICACION = :modificacion,
15 PAS_ELIMINADO = :eliminado
16 WHERE PAS_UUID = :uuid"
17 );
18 $stmt->execute([
19 ":uuid" => $modelo->uuid,
20 ":nombre" => $modelo->nombre,
21 ":modificacion" => $modelo->modificacion,
22 ":eliminado" => $modelo->eliminado ? 1 : 0
23 ]);
24}
25

C. Carpeta « srv / modelo »

1. srv / modelo / leePasatiempo.php

1<?php
2
3require_once __DIR__ . "/Pasatiempo.php";
4
5function leePasatiempo($objeto)
6{
7
8 if (!isset($objeto->nombre) || !is_string($objeto->nombre))
9 throw new ProblemDetails(
10 status: ProblemDetails::BadRequest,
11 type: "/error/nombreincorrecto.html",
12 title: "El nombre debe ser texto.",
13 );
14
15 if (!isset($objeto->uuid) || !is_string($objeto->uuid))
16 throw new ProblemDetails(
17 status: ProblemDetails::BadRequest,
18 type: "/error/uuidincorrecto.html",
19 title: "El uuid debe ser texto.",
20 );
21
22 if (!isset($objeto->eliminado) || !is_bool($objeto->eliminado))
23 throw new ProblemDetails(
24 status: ProblemDetails::BadRequest,
25 type: "/error/eliminadoincorrecto.html",
26 title: "El campo eliminado debe ser booleano.",
27 );
28
29 if (!isset($objeto->modificacion) || !is_int($objeto->modificacion))
30 throw new ProblemDetails(
31 status: ProblemDetails::BadRequest,
32 type: "/error/modificacionincorrecta.html",
33 title: "La modificacion debe ser número.",
34 );
35
36 return new Pasatiempo(
37 uuid: $objeto->uuid,
38 nombre: $objeto->nombre,
39 modificacion: $objeto->modificacion,
40 eliminado: $objeto->eliminado
41 );
42}
43

2. srv / modelo / Pasatiempo.php

1<?php
2
3class Pasatiempo
4{
5 public string $uuid;
6 public string $nombre;
7 public int $modificacion;
8 public bool $eliminado;
9
10 public function __construct(
11 string $nombre = "",
12 string $uuid = "",
13 int $modificacion = 0,
14 bool $eliminado = true
15 ) {
16 $this->nombre = $nombre;
17 $this->uuid = $uuid;
18 $this->modificacion = $modificacion;
19 $this->eliminado = $eliminado;
20 }
21
22 public function valida()
23 {
24 if ($this->uuid === "")
25 throw new Exception("Falta el uuid.");
26 if ($this->nombre === "")
27 throw new Exception("Falta el nombre.");
28 }
29}
30

O. Carpeta « lib »

Versión para imprimir.

A. Carpeta « lib / js »

1. lib / js / bdConsulta.js

1/**
2 * @template T
3 * @param { Promise<IDBDatabase> } bd
4 * @param { string[] } entidades
5 * @param { (
6 * transaccion: IDBTransaction,
7 * resolve: (resultado:T)=>void
8 * ) => void } consulta
9 * @returns {Promise<T>}
10 */
11export async function bdConsulta(bd, entidades, consulta) {
12
13 const base = await bd
14
15 return new Promise((resolve, reject) => {
16 // Inicia una transacción de solo lectura.
17 const transaccion = base.transaction(entidades, "readonly")
18 // Al terminar con error ejecuta la función reject.
19 transaccion.onerror = () => reject(transaccion.error)
20 // Estas son las operaciones para realizar la consulta.
21 consulta(transaccion, resolve)
22 })
23
24}

2. lib / js / bdEjecuta.js

1/**
2 * @param { Promise<IDBDatabase> } bd
3 * @param { string[] } entidades
4 * @param { (t:IDBTransaction) => void } operaciones
5 */
6export 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}

3. lib / js / confirmaEliminar.js

1export function confirmaEliminar() {
2 return confirm("Confirma la eliminación")
3}
4
5// Permite que los eventos de html usen la función.
6window["confirmaEliminar"] = confirmaEliminar

4. lib / js / enviaJson.js

1import { invocaServicio } from "./invocaServicio.js"
2
3/**
4 * @param { string } url
5 * @param { Object } body
6 * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS"
7 * | "CONNECT" | "HEAD" } metodoHttp
8 */
9export async function enviaJson(url, body, metodoHttp = "POST") {
10 return await invocaServicio(fetch(url, {
11 method: metodoHttp,
12 headers: {
13 "Content-Type": "application/json",
14 "Accept": "application/json, application/problem+json"
15 },
16 body: JSON.stringify(body)
17 }))
18}
19
20// Permite que los eventos de html usen la función.
21window["enviaJson"] = enviaJson

5. lib / js / invocaServicio.js

1import {
2 JsonResponse, JsonResponse_Created, JsonResponse_NoContent, JsonResponse_OK
3} from "./JsonResponse.js"
4import {
5 ProblemDetails, ProblemDetails_InternalServerError
6} from "./ProblemDetails.js"
7
8/**
9 * Espera a que la promesa de un fetch termine. Si
10 * hay error, lanza una excepción. Si no hay error,
11 * interpreta la respuesta del servidor como JSON y
12 * la convierte en una literal de objeto.
13 * @param { string | Promise<Response> } servicio
14 */
15export async function invocaServicio(servicio) {
16 let f = servicio
17 if (typeof servicio === "string") {
18 f = fetch(servicio, {
19 headers: { "Accept": "application/json, application/problem+json" }
20 })
21 } else if (!(f instanceof Promise)) {
22 throw new Error("Servicio de tipo incorrecto.")
23 }
24 const respuesta = await f
25 if (respuesta.ok) {
26 if (respuesta.status === JsonResponse_NoContent) {
27 return new JsonResponse(JsonResponse_NoContent)
28 }
29 const texto = await respuesta.text()
30 try {
31 const body = JSON.parse(texto)
32 if (respuesta.status === JsonResponse_Created) {
33 const location = respuesta.headers.get("location")
34 return new JsonResponse(JsonResponse_Created, body,
35 location === null ? undefined : location)
36 } else {
37 return new JsonResponse(JsonResponse_OK, body)
38 }
39 } catch (error) {
40 // El contenido no es JSON. Probablemente sea texto.
41 throw new ProblemDetails(ProblemDetails_InternalServerError,
42 "Problema interno en el servidor.", texto)
43 }
44 } else {
45 const texto = await respuesta.text()
46 try {
47 const { type, title, detail } = JSON.parse(texto)
48 throw new ProblemDetails(respuesta.status,
49 typeof title === "string" ? title : "",
50 typeof detail === "string" ? detail : undefined,
51 typeof type === "string" ? type : undefined)
52 } catch (error) {
53 if (error instanceof ProblemDetails) {
54 throw error
55 } else {
56 // El contenido no es JSON. Probablemente sea texto.
57 throw new ProblemDetails(respuesta.status, respuesta.statusText, texto)
58 }
59 }
60 }
61}
62
63// Permite que los eventos de html usen la función.
64window["invocaServicio"] = invocaServicio

6. lib / js / JsonResponse.js

1export const JsonResponse_OK = 200
2export const JsonResponse_Created = 201
3export const JsonResponse_NoContent = 204
4
5export class JsonResponse {
6
7 /**
8 * @param {number} status
9 * @param {any} [body]
10 * @param {string} [location]
11 */
12 constructor(status, body, location) {
13 /** @readonly */
14 this.status = status
15 /** @readonly */
16 this.body = body
17 /** @readonly */
18 this.location = location
19 }
20
21}

7. lib / js / muestraError.js

1import { ProblemDetails } from "./ProblemDetails.js"
2
3/**
4 * Muestra un error en la consola y en un cuadro de
5 * alerta el mensaje de una excepción.
6 * @param { ProblemDetails | Error | null } error descripción del error.
7 */
8export function muestraError(error) {
9 if (error === null) {
10 console.log("Error")
11 alert("Error")
12 } else if (error instanceof ProblemDetails) {
13 let mensaje = error.title
14 if (error.detail) {
15 mensaje += `\n\n${error.detail}`
16 }
17 mensaje += `\n\nCódigo: ${error.status}`
18 if (error.type) {
19 mensaje += ` ${error.type}`
20 }
21 console.error(mensaje)
22 console.error(error)
23 alert(mensaje)
24 } else {
25 console.error(error)
26 alert(error.message)
27 }
28}
29
30// Permite que los eventos de html usen la función.
31window["muestraError"] = muestraError

8. lib / js / muestraObjeto.js

1/**
2 * @param { Document | HTMLElement } raizHtml
3 * @param { any } objeto
4 */
5export async 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 const elementoHtml = buscaElementoHtml(raizHtml, nombre)
11 if (elementoHtml instanceof HTMLImageElement) {
12 await muestraImagen(raizHtml, elementoHtml, definiciones)
13 } else if (elementoHtml !== null) {
14 for (const [atributo, valor] of Object.entries(definiciones)) {
15 if (atributo in elementoHtml) {
16 elementoHtml[atributo] = valor
17 }
18 }
19 }
20 }
21 }
22}
23// Permite que los eventos de html usen la función.
24window["muestraObjeto"] = muestraObjeto
25
26/**
27 * @param { Document | HTMLElement } raizHtml
28 * @param { string } nombre
29 */
30export function buscaElementoHtml(raizHtml, nombre) {
31 return raizHtml.querySelector(
32 `#${nombre},[name="${nombre}"],[data-name="${nombre}"]`)
33}
34
35/**
36 * @param { Document | HTMLElement } raizHtml
37 * @param { string } propiedad
38 * @param {any[]} valores
39 */
40function muestraArray(raizHtml, propiedad, valores) {
41 const conjunto = new Set(valores)
42 const elementos =
43 raizHtml.querySelectorAll(`[name="${propiedad}"],[data-name="${propiedad}"]`)
44 if (elementos.length === 1) {
45 const elemento = elementos[0]
46 if (elemento instanceof HTMLSelectElement) {
47 const options = elemento.options
48 for (let i = 0, len = options.length; i < len; i++) {
49 const option = options[i]
50 option.selected = conjunto.has(option.value)
51 }
52 return
53 }
54 }
55 for (let i = 0, len = elementos.length; i < len; i++) {
56 const elemento = elementos[i]
57 if (elemento instanceof HTMLInputElement) {
58 elemento.checked = conjunto.has(elemento.value)
59 }
60 }
61}
62
63/**
64 * @param { Document | HTMLElement } raizHtml
65 * @param { HTMLImageElement } img
66 * @param { any } definiciones
67 */
68async function muestraImagen(raizHtml, img, definiciones) {
69 const input = getInputParaElementoHtml(raizHtml, img)
70 const src = definiciones.src
71 if (src !== undefined) {
72 img.dataset.inicial = src
73 if (input === null) {
74 img.src = src
75 if (src === "") {
76 img.hidden = true
77 } else {
78 img.hidden = false
79 }
80 } else {
81 const dataUrl = await getDataUrlDeSeleccion(input)
82 if (dataUrl !== "") {
83 img.hidden = false
84 img.src = dataUrl
85 } else if (src === "") {
86 img.src = ""
87 img.hidden = true
88 } else {
89 img.src = src
90 img.hidden = false
91 }
92 }
93 }
94 for (const [atributo, valor] of Object.entries(definiciones)) {
95 if (atributo !== "src" && atributo in img) {
96 img[atributo] = valor
97 }
98 }
99}
100
101/**
102 * @param { HTMLInputElement } input
103 */
104export function getArchivoSeleccionado(input) {
105 const seleccion = input.files
106 if (seleccion === null || seleccion.length === 0) {
107 return null
108 } else {
109 return seleccion.item(0)
110 }
111}
112// Permite que los eventos de html usen la función.
113window["getArchivoSeleccionado"] = getArchivoSeleccionado
114
115
116/**
117 * @param {HTMLInputElement} input
118 * @returns {Promise<string>}
119 */
120export function getDataUrlDeSeleccion(input) {
121 return new Promise((resolve, reject) => {
122 try {
123 const seleccion = getArchivoSeleccionado(input)
124 if (seleccion === null) {
125 resolve("")
126 } else {
127 const reader = new FileReader()
128 reader.onload = () => {
129 const dataUrl = reader.result
130 if (typeof dataUrl === "string") {
131 resolve(dataUrl)
132 } else {
133 resolve("")
134 }
135 }
136 reader.onerror = () => reject(reader.error)
137 reader.readAsDataURL(seleccion)
138 }
139 } catch (error) {
140 resolve(error)
141 }
142 })
143}
144// Permite que los eventos de html usen la función.
145window["getDataUrlDeSeleccion"] = getDataUrlDeSeleccion
146
147/**
148 * @param { Document | HTMLElement } raizHtml
149 * @param { HTMLElement } elementoHtml
150 */
151export function getInputParaElementoHtml(raizHtml, elementoHtml) {
152 const inputId = elementoHtml.getAttribute("data-input")
153 if (inputId === null) {
154 return null
155 } else {
156 const input = buscaElementoHtml(raizHtml, inputId)
157 if (input instanceof HTMLInputElement) {
158 return input
159 } else {
160 return null
161 }
162 }
163}

9. lib / js / ProblemDetails.js

1export const ProblemDetails_BadRequest = 400
2export const ProblemDetails_NotFound = 404
3export const ProblemDetails_InternalServerError = 500
4
5export class ProblemDetails extends Error {
6
7 /**
8 * @param {number} status
9 * @param {string} title
10 * @param {string} [detail]
11 * @param {string} [type]
12 */
13 constructor(status, title, detail, type) {
14 super(title)
15 /** @readonly */
16 this.status = status
17 /** @readonly */
18 this.type = type
19 /** @readonly */
20 this.title = title
21 /** @readonly */
22 this.detail = detail
23 }
24
25}

10. lib / js / registraServiceWorkerSiEsSoportado.js

1import { muestraError } from "./muestraError.js"
2
3/**
4 * @param { string | URL } urlDeServiceWorker
5 */
6export async function registraServiceWorkerSiEsSoportado(urlDeServiceWorker) {
7 try {
8 if ("serviceWorker" in navigator) {
9 const registro = await navigator.serviceWorker.register(urlDeServiceWorker)
10 console.log(urlDeServiceWorker, "registrado.")
11 console.log(registro)
12 }
13 } catch (error) {
14 muestraError(error)
15 }
16}

B. Carpeta « lib / php »

1. lib / php / ejecutaServicio.php

1<?php
2
3require_once __DIR__ . "/JsonResponse.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6/**
7 * Ejecuta una funcion que implementa un servicio.
8 */
9function ejecutaServicio($servicio)
10{
11 try {
12 $resultado = $servicio();
13 if (!($resultado instanceof JsonResponse)) {
14 $resultado = JsonResponse::ok($resultado);
15 }
16 procesa_json_response($resultado);
17 } catch (ProblemDetails $details) {
18 procesa_problem_details($details);
19 } catch (Throwable $throwable) {
20 procesa_problem_details(new ProblemDetails(
21 status: ProblemDetails::InternalServerError,
22 type: "/error/errorinterno.html",
23 title: "Error interno del servidor.",
24 detail: $throwable->getMessage()
25 ));
26 }
27}
28
29function procesa_json_response(JsonResponse $response)
30{
31 $json = "";
32 $body = $response->body;
33 if ($response->status !== JsonResponse_NoContent) {
34 $json = json_encode($body);
35 if ($json === false) {
36 no_puede_generar_json();
37 return;
38 }
39 }
40 http_response_code($response->status);
41 if ($response->location !== null) {
42 header("Location: {$response->location}");
43 }
44 if ($response->status !== JsonResponse_NoContent) {
45 header("Content-Type: application/json");
46 echo $json;
47 }
48}
49
50function procesa_problem_details(ProblemDetails $details)
51{
52 $body = ["title" => $details->title];
53 if ($details->type !== null) {
54 $body["type"] = $details->type;
55 }
56 if ($details->detail !== null) {
57 $body["detail"] = $details->detail;
58 }
59 $json = json_encode($body);
60 if ($json === false) {
61 no_puede_generar_json();
62 } else {
63 http_response_code($details->status);
64 header("Content-Type: application/problem+json");
65 echo $json;
66 }
67}
68
69function no_puede_generar_json()
70{
71 http_response_code(ProblemDetails::InternalServerError);
72 header("Content-Type: application/problem+json");
73 echo '{"type":"/error/nojson.html"'
74 . ',"title":"El valor devuelto no puede representarse como JSON."}';
75}
76

2. lib / php / JsonResponse.php

1<?php
2
3const JsonResponse_OK = 200;
4const JsonResponse_Created = 201;
5const JsonResponse_NoContent = 204;
6
7class JsonResponse
8{
9
10 public int $status;
11 public $body;
12 public ?string $location;
13
14 public function __construct(
15 int $status = JsonResponse_OK,
16 $body = null,
17 ?string $location = null
18 ) {
19 $this->status = $status;
20 $this->body = $body;
21 $this->location = $location;
22 }
23
24 public static function ok($body)
25 {
26 return new JsonResponse(body: $body);
27 }
28
29 public static function created(string $location, $body)
30 {
31 return new JsonResponse(JsonResponse_Created, $body, $location);
32 }
33
34 public static function noContent()
35 {
36 return new JsonResponse(JsonResponse_NoContent, null);
37 }
38}
39

3. lib / php / leeJson.php

1<?php
2
3function leeJson()
4{
5 return json_decode(file_get_contents("php://input"));
6}
7

4. lib / php / ProblemDetails.php

1<?php
2
3class ProblemDetails extends Exception
4{
5
6 public const BadRequest = 400;
7 public const NotFound = 404;
8 public const InternalServerError = 500;
9
10 public int $status;
11 public string $title;
12 public ?string $type;
13 public ?string $detail;
14
15 public function __construct(
16 int $status,
17 string $title,
18 ?string $type = null,
19 ?string $detail = null,
20 Throwable $previous = null
21 ) {
22 parent::__construct($title, $status, $previous);
23 $this->status = $status;
24 $this->type = $type;
25 $this->title = $title;
26 $this->detail = $detail;
27 }
28}
29

5. lib / php / recibeFetchAll.php

1<?php
2
3function recibeFetchAll(false|array $resultado): array
4{
5 if ($resultado === false) {
6 return [];
7 } else {
8 return $resultado;
9 }
10}
11

P. Carpeta « error »

Versión para imprimir.

A. error / eliminadoincorrecto.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 booleano</title>
10
11<body>
12
13 <h1>El campo eliminado debe ser booleano</h1>
14
15</body>
16
17</html>

B. error / errorinterno.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>

C. error / modificacionincorrecta.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>

D. error / nojson.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 valor devuelto no puede representarse como JSON</title>
10
11</head>
12
13<body>
14
15 <h1>El valor devuelto no puede representarse como JSON</h1>
16
17 <p>
18 Debido a un error interno del servidor, la respuesta generada, no se puede
19 recuperar.
20 </p>
21
22</body>
23
24</html>

E. error / nombreincorrecto.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>

F. error / uuidincorrecto.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 uuid debe ser texto</title>
10
11<body>
12
13 <h1>El uuid debe ser texto</h1>
14
15</body>
16
17</html>

Q. jsconfig.json

1{
2 "compilerOptions": {
3 "checkJs": true,
4 "strictNullChecks": true,
5 "target": "ES6",
6 "module": "ES6",
7 "moduleResolution": "classic",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

R. Resumen