13. 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. Prueba el ejemplo en https://sincro.rf.gd/.

  2. Copia la url de la app y pégala en varios navegadores y dispositivos.

  3. Las modificaciones que realices en dispositivo o navegador se verán reflejados en los otros dispositivos, en un máximo de 20 segundos.

  4. 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.

  5. Descarga el archivo /src/sincro.zip y descompáctalo.

  6. Crea tu proyecto en GitHub:

    1. Crea una cuenta de email, por ejemplo, pepito@google.com

    2. 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.

    3. Crea un repositorio nuevo. En la página principal de GitHub cliquea 📘 New.

    4. 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.

  7. Importa el proyecto en GitHub:

    1. 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.

    2. En Visual Studio Code, usa el botón de la izquierda para Source Control.

      Imagen de Source Control
    3. Cliquea el botón Clone Repository.

    4. Pega la url que copiaste anteriormente hasta arriba, donde dice algo como Provide repository URL y presiona la teclea Intro.

    5. Selecciona la carpeta donde se guardará la carpeta del proyecto.

    6. Abre la carpeta del proyecto importado.

    7. Añade el contenido de la carpeta descompactada que contiene el código del ejemplo.

  8. Edita los archivos que desees.

  9. 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.

  10. 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.

  11. Haz clic derecho en index.html, selecciona PHP Server: serve project y se abre el navegador para que puedas probar localmente el ejemplo.

  12. Cuando desarrolles, es incómodo modificar la versión cada que realizas cambios; en ves de ello desinstala la app:

    1. Abre las herramientas de depuración haciendo clic derecho en la página y selecciona Inspeccionar (o Inspect si aparece en inglés).

    2. En la Pestaña Aplicación (o Application en inglés) selecciona Almacenamoento (o Storage en inglés). Cliquea Borrar datos del sitio.

    3. Recarga la app, de preferencia haciendo clic derecho en el ícono de volver a cargar la página Ïmagen del ícono de recarga 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 Ïmagen del ícono de recarga. Revisa que no aparezca ningún error ni en la pestañas Consola, ni en Red.

    4. 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.

    5. 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.

  13. Para depurar paso a paso haz lo siguiente:

    1. En el navegador, haz clic derecho en la página que deseas depurar y selecciona inspeccionar.

    2. Selecciona la pestaña Fuentes (o Sources si tu navegador está en Inglés).

    3. Selecciona el archivo donde vas a empezar a depurar.

    4. Haz clic en el número de la línea donde vas a empezar a depurar.

    5. En Visual Studio Code, abre el archivo de PHP donde vas a empezar a depurar.

    6. Haz clic en Run and Debug .

    7. Si no está configurada la depuración, haz clic en create a launch json file.

    8. Haz clic en la flechita RUN AND DEBUG, al lado de la cual debe decir Listen for Xdebug .

    9. Aparece un cuadro con los controles de depuración

    10. Selecciona otra vez el archivo de PHP y haz clic en el número de la línea donde vas a empezar a depurar.

    11. Regresa al navegador, recarga la página de manera normal y empieza a usarla.

    12. 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.

  14. Sube el proyecto al hosting que elijas sin incluir el archivo .htaccess. En algunos casos puedes usar filezilla (https://filezilla-project.org/)

  15. En algunos host como InfinityFree, tienes que configurar el certificado SSL.

  16. 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.

  17. Abre un navegador y prueba el proyecto en tu hosting.

  18. 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.

  19. 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.

    Imagen de Commit & Push

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
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>

G. 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/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>

H. 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="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>

I. 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 .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
265. 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
356. 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
467. 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
528. 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
639. 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
7310. 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 "/"

J. archivos.txt

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",

K. 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"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.
73if (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
93async 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 */
109async 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}

L. Carpeta « js »

Versión para imprimir.

A. js / esperaUnPocoYSincroniza.js

1import { exportaAHtml } from "../lib/js/exportaAHtml.js"
2import { 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 */
9const MILISEGUNDOS_PARA_VOLVER_A_SINCRONIZAR = 20000
10
11/**
12 * @param {HTMLUListElement} lista
13 */
14export function esperaUnPocoYSincroniza(lista) {
15 setTimeout(() => sincroniza(lista), MILISEGUNDOS_PARA_VOLVER_A_SINCRONIZAR)
16}
17
18exportaAHtml(esperaUnPocoYSincroniza)

B. js / registraServiceWorker.js

1"use strict" // usa JavaScript en modo estricto.
2
3const nombreDeServiceWorker = "sw.js"
4
5try {
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}

C. js / renderiza.js

1import { exportaAHtml } from "../lib/js/exportaAHtml.js"
2import { htmlentities } from "../lib/js/htmlentities.js"
3
4/**
5 * @param {HTMLUListElement} lista
6 * @param {import("./modelo/PASATIEMPO.js").PASATIEMPO[]} pasatiempos
7 */
8export 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
24exportaAHtml(renderiza)

D. js / sincroniza.js

1import { enviaJson } from "../lib/js/enviaJson.js"
2import { exportaAHtml } from "../lib/js/exportaAHtml.js"
3import { muestraError } from "../lib/js/muestraError.js"
4import { pasatiempoConsultaTodos } from "./bd/pasatiempoConsultaTodos.js"
5import { pasatiemposReemplaza } from "./bd/pasatiemposReemplaza.js"
6import { esperaUnPocoYSincroniza } from "./esperaUnPocoYSincroniza.js"
7import { validaPasatiempos } from "./modelo/validaPasatiempos.js"
8import { renderiza } from "./renderiza.js"
9
10/**
11 * @param {HTMLUListElement} lista
12 */
13export 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
29exportaAHtml(sincroniza)

E. Carpeta « js / bd »

1. js / bd / Bd.js

1export const ALMACEN_PASATIEMPO = "PASATIEMPO"
2export const PAS_ID = "PAS_ID"
3export const INDICE_NOMBRE = "INDICE_NOMBRE"
4export const PAS_NOMBRE = "PAS_NOMBRE"
5const BD_NOMBRE = "sincronizacion"
6const BD_VERSION = 1
7
8/** @type { Promise<IDBDatabase> } */
9export 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})

2. js / bd / pasatiempoAgrega.js

1import { bdEjecuta } from "../../lib/js/bdEjecuta.js"
2import { creaIdCliente } from "../../lib/js/creaIdCliente.js"
3import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js"
4import { validaNombre } from "../modelo/validaNombre.js"
5import { exportaAHtml } from "../../lib/js/exportaAHtml.js"
6
7/**
8 * @param {import("../modelo/PASATIEMPO.js").PASATIEMPO} modelo
9 */
10export 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
22exportaAHtml(pasatiempoAgrega)

3. js / bd / pasatiempoBusca.js

1import { bdConsulta } from "../../lib/js/bdConsulta.js"
2import { exportaAHtml } from "../../lib/js/exportaAHtml.js"
3import { validaPasatiempo } from "../modelo/validaPasatiempo.js"
4import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js"
5
6/**
7 * @param {string} id
8 */
9export 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
43exportaAHtml(pasatiempoBusca)

4. js / bd / pasatiempoConsultaNoEliminados.js

1import { bdConsulta } from "../../lib/js/bdConsulta.js"
2import { exportaAHtml } from "../../lib/js/exportaAHtml.js"
3import { validaPasatiempo } from "../modelo/validaPasatiempo.js"
4import { ALMACEN_PASATIEMPO, Bd, INDICE_NOMBRE } from "./Bd.js"
5
6export 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
53exportaAHtml(pasatiempoConsultaNoEliminados)

5. js / bd / pasatiempoConsultaTodos.js

1import { bdConsulta } from "../../lib/js/bdConsulta.js"
2import { validaPasatiempo } from "../modelo/validaPasatiempo.js"
3import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js"
4
5/**
6 * Lista todos los objetos, incluyendo los que tienen borrado lógico.
7 */
8export 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}

6. js / bd / pasatiempoElimina.js

1import { bdEjecuta } from "../../lib/js/bdEjecuta.js"
2import { exportaAHtml } from "../../lib/js/exportaAHtml.js"
3import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js"
4import { pasatiempoBusca } from "./pasatiempoBusca.js"
5
6/**
7 * @param { string } id
8 */
9export 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
21exportaAHtml(pasatiempoElimina)

7. js / bd / pasatiempoModifica.js

1import { bdEjecuta } from "../../lib/js/bdEjecuta.js"
2import { exportaAHtml } from "../../lib/js/exportaAHtml.js"
3import { validaId } from "../modelo/validaId.js"
4import { validaNombre } from "../modelo/validaNombre.js"
5import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js"
6import { pasatiempoBusca } from "./pasatiempoBusca.js"
7
8/**
9 * @param { import("../modelo/PASATIEMPO.js").PASATIEMPO } modelo
10 */
11export 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
27exportaAHtml(pasatiempoModifica)

8. js / bd / pasatiemposReemplaza.js

1import { bdEjecuta } from "../../lib/js/bdEjecuta.js"
2import { 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 */
8export 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}

F. Carpeta « js / modelo »

1. js / modelo / PASATIEMPO.js

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 */

2. js / modelo / validaId.js

1/**
2 * @param {string} id
3 */
4export function validaId(id) {
5 if (id === "")
6 throw new Error("Falta el id.")
7 }

3. js / modelo / validaNombre.js

1/**
2 * @param {string} nombre
3 */
4export function validaNombre(nombre) {
5 if (nombre === "")
6 throw new Error("Falta el nombre.")
7}

4. js / modelo / validaPasatiempo.js

1/**
2 * @param { any } objeto
3 * @returns {import("./PASATIEMPO.js").PASATIEMPO}
4 */
5export 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}

5. js / modelo / validaPasatiempos.js

1import { validaPasatiempo } from "./validaPasatiempo.js"
2
3/**
4 * @param { any } objetos
5 * @returns {import("./PASATIEMPO.js").PASATIEMPO[]}
6 */
7export 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}

M. Carpeta « srv »

Versión para imprimir.

A. srv / sincroniza.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/recuperaJson.php";
5require_once __DIR__ . "/../lib/php/devuelveJson.php";
6require_once __DIR__ . "/../lib/php/ProblemDetails.php";
7require_once __DIR__ . "/../lib/php/devuelveProblemDetails.php";
8require_once __DIR__ . "/../lib/php/devuelveErrorInterno.php";
9require_once __DIR__ . "/modelo/TABLA_PASATIEMPO.php";
10require_once __DIR__ . "/modelo/validaPasatiempo.php";
11require_once __DIR__ . "/bd/pasatiempoAgrega.php";
12require_once __DIR__ . "/bd/pasatiempoBusca.php";
13require_once __DIR__ . "/bd/pasatiempoConsultaNoEliminados.php";
14require_once __DIR__ . "/bd/pasatiempoModifica.php";
15
16ejecutaServicio(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

B. Carpeta « srv / bd »

1. srv / bd / Bd.php

1<?php
2
3class 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

2. srv / bd / pasatiempoAgrega.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/validaNombre.php";
4require_once __DIR__ . "/../../lib/php/insert.php";
5require_once __DIR__ . "/Bd.php";
6require_once __DIR__ . "/../modelo/TABLA_PASATIEMPO.php";
7require_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 */
17function 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

3. srv / bd / pasatiempoBusca.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/selectFirst.php";
4require_once __DIR__ . "/Bd.php";
5require_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 */
15function pasatiempoBusca(string $id): false|array
16{
17 return selectFirst(
18 pdo: Bd::pdo(),
19 from: PASATIEMPO,
20 where: [PAS_ID => $id]
21 );
22}
23

4. srv / bd / pasatiempoConsultaNoEliminados.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/select.php";
4require_once __DIR__ . "/Bd.php";
5require_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 */
15function pasatiempoConsultaNoEliminados()
16{
17 return select(
18 pdo: Bd::pdo(),
19 from: PASATIEMPO,
20 where: [PAS_ELIMINADO => 0],
21 orderBy: PAS_NOMBRE
22 );
23}
24

5. srv / bd / pasatiempoModifica.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/validaNombre.php";
4require_once __DIR__ . "/../../lib/php/update.php";
5require_once __DIR__ . "/Bd.php";
6require_once __DIR__ . "/../modelo/TABLA_PASATIEMPO.php";
7require_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 */
17function 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

C. Carpeta « srv / modelo »

1. srv / modelo / TABLA_PASATIEMPO.php

1<?php
2
3const PASATIEMPO = "PASATIEMPO";
4const PAS_ID = "PAS_ID";
5const PAS_NOMBRE = "PAS_NOMBRE";
6const PAS_MODIFICACION = "PAS_MODIFICACION";
7const PAS_ELIMINADO = "PAS_ELIMINADO";
8

2. srv / modelo / validaId.php

1<?php
2
3function 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

3. srv / modelo / validaPasatiempo.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/BAD_REQUEST.php";
4require_once __DIR__ . "/../../lib/php/validaJson.php";
5require_once __DIR__ . "/../../lib/php/ProblemDetails.php";
6require_once __DIR__ . "/TABLA_PASATIEMPO.php";
7
8function 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

N. 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[]} almacenes
5 * @param {(transaccion: IDBTransaction, resolve: (resultado:T)=>void) => any
6 * } consulta
7 * @returns {Promise<T>}
8 */
9export 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}

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 / consumeJson.js

1import { exportaAHtml } from "./exportaAHtml.js"
2import { 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 */
12export 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
96exportaAHtml(consumeJson)

4. lib / js / creaIdCliente.js

1import { 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 */
7export 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
15exportaAHtml(creaIdCliente)

5. lib / js / enviaJson.js

1import { consumeJson } from "./consumeJson.js"
2import { 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 */
10export 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
21exportaAHtml(enviaJson)

6. lib / js / exportaAHtml.js

1/**
2 * Permite que los eventos de html usen la función.
3 * @param {function} functionInstance
4 */
5export function exportaAHtml(functionInstance) {
6 window[nombreDeFuncionParaHtml(functionInstance)] = functionInstance
7}
8
9/**
10 * @param {function} valor
11 */
12export function nombreDeFuncionParaHtml(valor) {
13 const names = valor.name.split(/\s+/g)
14 return names[names.length - 1]
15}

7. lib / js / htmlentities.js

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*/
8export 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

8. lib / js / muestraError.js

1import { exportaAHtml } from "./exportaAHtml.js"
2import { 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 */
9export 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
42exportaAHtml(muestraError)

9. lib / js / muestraObjeto.js

1import { exportaAHtml } from "./exportaAHtml.js"
2
3/**
4 * @param { Document | HTMLElement } raizHtml
5 * @param { any } objeto
6 */
7export 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}
38exportaAHtml(muestraObjeto)
39
40/**
41 * @param { Document | HTMLElement } raizHtml
42 * @param { string } nombre
43 */
44export 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 */
54function 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 */
88function 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 */
120export 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}

10. lib / js / ProblemDetails.js

1/**
2 * Detalle de los errores devueltos por un servicio.
3 */
4export 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}

B. Carpeta « lib / php »

1. lib / php / BAD_REQUEST.php

1<?php
2
3const BAD_REQUEST = 400;
4

2. lib / php / calculaArregloDeParametros.php

1<?php
2
3function calculaArregloDeParametros(array $arreglo)
4{
5 $parametros = [];
6 foreach ($arreglo as $llave => $valor) {
7 $parametros[":$llave"] = $valor;
8 }
9 return $parametros;
10}
11

3. lib / php / calculaSqlDeAsignaciones.php

1<?php
2
3function 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

4. lib / php / calculaSqlDeCamposDeInsert.php

1<?php
2
3function 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

5. lib / php / calculaSqlDeValues.php

1<?php
2
3function 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

6. lib / php / delete.php

1<?php
2
3require_once __DIR__ . "/calculaArregloDeParametros.php";
4require_once __DIR__ . "/calculaSqlDeAsignaciones.php";
5
6function 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

7. lib / php / devuelveErrorInterno.php

1<?php
2
3require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php";
4require_once __DIR__ . "/devuelveProblemDetails.php";
5require_once __DIR__ . "/devuelveProblemDetails.php";
6
7function 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

8. lib / php / devuelveJson.php

1<?php
2
3require_once __DIR__ . "/devuelveResultadoNoJson.php";
4
5function 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

9. lib / php / devuelveProblemDetails.php

1<?php
2
3require_once __DIR__ . "/devuelveResultadoNoJson.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6function 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

10. lib / php / devuelveResultadoNoJson.php

1<?php
2
3require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php";
4
5function 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

11. lib / php / ejecutaServicio.php

1<?php
2
3require_once __DIR__ . "/ProblemDetails.php";
4require_once __DIR__ . "/devuelveProblemDetails.php";
5require_once __DIR__ . "/devuelveErrorInterno.php";
6
7function 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

12. lib / php / fetch.php

1<?php
2
3function 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

13. lib / php / fetchAll.php

1<?php
2
3function 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

14. lib / php / insert.php

1<?php
2
3require_once __DIR__ . "/calculaSqlDeCamposDeInsert.php";
4require_once __DIR__ . "/calculaSqlDeValues.php";
5require_once __DIR__ . "/calculaArregloDeParametros.php";
6
7function 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

15. lib / php / INTERNAL_SERVER_ERROR.php

1<?php
2
3const INTERNAL_SERVER_ERROR = 500;

16. lib / php / ProblemDetails.php

1<?php
2
3/** Detalle de los errores devueltos por un servicio. */
4class 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

17. lib / php / recuperaJson.php

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

18. lib / php / select.php

1<?php
2
3require_once __DIR__ . "/fetchAll.php";
4require_once __DIR__ . "/calculaSqlDeAsignaciones.php";
5
6function 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

19. lib / php / selectFirst.php

1<?php
2
3require_once __DIR__ . "/fetch.php";
4require_once __DIR__ . "/calculaArregloDeParametros.php";
5require_once __DIR__ . "/calculaSqlDeAsignaciones.php";
6
7function 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

20. lib / php / update.php

1<?php
2
3require_once __DIR__ . "/calculaArregloDeParametros.php";
4require_once __DIR__ . "/calculaSqlDeAsignaciones.php";
5
6
7function 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

21. lib / php / validaJson.php

1<?php
2
3require_once __DIR__ . "/BAD_REQUEST.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6function 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

22. lib / php / validaNombre.php

1<?php
2
3require_once __DIR__ . "/BAD_REQUEST.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6function 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

O. Carpeta « error »

Versión para imprimir.

A. error / datosnojson.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>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>

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

C. 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>

D. error / faltaid.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>

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

F. error / idincorrecto.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>

G. 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>

H. error / nombreenblanco.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>

I. 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>

J. error / resultadonojson.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>

P. .htaccess

1AddType application/manifest+json .webmanifest
2
3ExpiresActive On
4
5Header set Cache-Control "max-age=1, must-revalidate"
6
7RewriteEngine On
8
9RewriteCond %{HTTP:X-Forwarded-Proto} !https
10RewriteCond %{HTTPS} off
11RewriteCond %{HTTP:CF-Visitor} !{"scheme":"https"}
12RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
13

Q. jsconfig.json

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}

R. Resumen