<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Stalin Maza Blog]]></title><description><![CDATA[En este blog me planteo compartir los pequeños conocimientos que adquiera y considere relevantes para que otras personas puedan usarlos, aprenderlos y aplicarlos en sus proyectos.]]></description><link>https://devblog.stalinmaza.com</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1559784860679/DEgjvsUNW.png</url><title>Stalin Maza Blog</title><link>https://devblog.stalinmaza.com</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 24 Apr 2026 16:17:47 GMT</lastBuildDate><atom:link href="https://devblog.stalinmaza.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Generar Descripciones Automáticas de Campos y SQL con Comentarios de una Base de Datos]]></title><description><![CDATA[En este artículo aprenderás cómo generar un diccionario de datos enriquecido a partir de tu base de datos, usando un modelo de lenguaje (por ejemplo, OpenFoundry o GPT) para describir cada campo, y có]]></description><link>https://devblog.stalinmaza.com/generar-descripciones-autom-ticas-de-campos-y-sql-con-comentarios-de-una-base-de-datos</link><guid isPermaLink="true">https://devblog.stalinmaza.com/generar-descripciones-autom-ticas-de-campos-y-sql-con-comentarios-de-una-base-de-datos</guid><dc:creator><![CDATA[Stalin Maza]]></dc:creator><pubDate>Tue, 24 Mar 2026 18:07:58 GMT</pubDate><content:encoded><![CDATA[<p>En este artículo aprenderás cómo generar un <strong>diccionario de datos enriquecido</strong> a partir de tu base de datos, usando un modelo de lenguaje (por ejemplo, OpenFoundry o GPT) para describir cada campo, y cómo convertir esos resultados en <strong>sentencias SQL con comentarios</strong> listos para aplicar sobre tu esquema.</p>
<p>El proceso combina extracción de metadatos SQL, generación automática de descripciones, manejo de archivos parciales y exportación final de scripts.</p>
<h2>Modelo utilizado para la prueba - OpenFoundry</h2>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761970249083/f5ee51ed-6811-49c4-8b7c-28df993e3d0f.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>🔍 1. Extracción de Metadatos desde la Base de Datos</h2>
<p>Primero, necesitamos obtener la <strong>estructura de las tablas y columnas</strong> de nuestra base de datos (en este caso MySQL) en un formato JSON uniforme, como este:</p>
<pre><code class="language-json">{
  "Tabla": "app_cloud_billing",
  "Campo": "tags",
  "TipoDeDato": "JSON",
  "Restricciones": "NULL",
  "Descripcion": ""
}
</code></pre>
<h3>Consulta SQL para extraer metadata de la base de datos</h3>
<p>Puedes obtener esta información directamente desde <code>INFORMATION_SCHEMA.COLUMNS</code>:</p>
<pre><code class="language-sql">SELECT 
    TABLE_NAME AS 'Tabla',
    COLUMN_NAME AS 'Campo',
    COLUMN_TYPE AS 'TipoDeDato',
    IS_NULLABLE AS 'Restricciones',
    COLUMN_COMMENT AS 'Descripcion'
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'tu_base_de_datos'
ORDER BY TABLE_NAME, COLUMN_NAME;
</code></pre>
<p>Luego exportas el resultado en formato JSON (puedes hacerlo con una herramienta como DBeaver, MySQL Workbench o mediante un script Python).</p>
<hr />
<h2>🤖 2. Generación Automática de Descripciones con un Modelo de Lenguaje</h2>
<p>Una vez tengas el archivo <code>metadata_database.json</code>, puedes usar un modelo como <strong>OpenFoundry</strong>, GPT u otro para generar descripciones automáticas.<br />El modelo recibirá algo como esto:</p>
<pre><code class="language-json">[
  {"Tabla": "app_cloud_billing", "Campo": "tags", "TipoDeDato": "JSON", "Restricciones": "NULL", "Descripcion": ""}
]
</code></pre>
<p>Y devolverá:</p>
<pre><code class="language-json">[
  {"Tabla": "app_cloud_billing", "Campo": "tags", "TipoDeDato": "JSON", "Restricciones": "NULL",
   "Descripcion": "Metadatos de facturación y atribución en formato JSON. Contiene etiquetas de clave/valor (ej. ambiente, proyecto, dueño) aplicadas al recurso"}
]
</code></pre>
<p>Para manejar grandes volúmenes de datos (más de 800 tokens), puedes dividir el archivo en <strong>bloques parciales</strong> y procesarlos por separado.</p>
<hr />
<h2>🧩 3. Guardar Resultados Parciales con Python</h2>
<p>Usa este script para <strong>guardar los registros procesados</strong> en archivos parciales con timestamp:</p>
<pre><code class="language-python">import json
from datetime import datetime
import os

with open("campos.json", "r", encoding="utf-8") as f:
    campos = json.load(f)

campos_con_datos = [c for c in campos if c.get("Descripcion")]

os.makedirs("parciales", exist_ok=True)
timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M")
output_path = f"parciales/diccionario_con_datos_{timestamp}.json"

with open(output_path, "w", encoding="utf-8") as f:
    json.dump(campos_con_datos, f, ensure_ascii=False, indent=2)

print(f"Archivo parcial generado en: {output_path}")
</code></pre>
<blockquote>
<p>💡 Esto facilita retomar el proceso si el modelo falla o se interrumpe.<br />Cada archivo parcial queda versionado por hora y minuto.</p>
</blockquote>
<hr />
<h2>🧮 4. Unificar Archivos Parciales</h2>
<p>Si has generado varios archivos parciales, puedes consolidarlos en un solo archivo ordenado por tabla y campo:</p>
<pre><code class="language-python">import json
import os

carpeta_parciales = "parciales"
archivos_json = [f for f in os.listdir(carpeta_parciales) if f.endswith(".json")]

datos_unificados = []

for archivo in archivos_json:
    with open(os.path.join(carpeta_parciales, archivo), "r", encoding="utf-8") as f:
        datos_unificados.extend(json.load(f))

# Eliminar duplicados y ordenar
diccionario_unificado = {(d["Tabla"], d["Campo"]): d for d in datos_unificados}
ordenado = sorted(diccionario_unificado.values(), key=lambda x: (x["Tabla"], x["Campo"]))

with open("resultado_final.json", "w", encoding="utf-8") as f:
    json.dump(ordenado, f, ensure_ascii=False, indent=2)

print(f"Archivo unificado generado: resultado_final.json")
</code></pre>
<hr />
<h2>🧱 5. Generar Scripts SQL con Comentarios</h2>
<p>Por último, puedes crear automáticamente los comandos SQL <code>ALTER TABLE</code> que agregan comentarios descriptivos a tus columnas.</p>
<p>Este script genera tanto un <strong>archivo SQL completo</strong> como <strong>archivos parciales por tabla</strong>:</p>
<pre><code class="language-python">import json
import os
from datetime import datetime

with open("resultado_final.json", "r", encoding="utf-8") as f:
    datos = json.load(f)

sql_por_tabla = {}
for item in datos:
    tabla = item["Tabla"]
    campo = item["Campo"]
    tipo = item["TipoDeDato"]
    restr = item["Restricciones"]
    desc = item["Descripcion"].replace("'", "''")  # escapar comillas simples

    sentencia = f"MODIFY COLUMN {campo} {tipo} {restr} COMMENT '{desc}'"
    sql_por_tabla.setdefault(tabla, []).append(sentencia)

os.makedirs("parciales_sql", exist_ok=True)
timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M")

sql_total = ""

for tabla, sentencias in sql_por_tabla.items():
    bloque = f"ALTER TABLE {tabla}\n  " + ",\n  ".join(sentencias) + ";\n\n"
    sql_total += bloque

    with open(f"parciales_sql/{tabla}_{timestamp}.sql", "w", encoding="utf-8") as f:
        f.write(bloque)

with open(f"diccionario_sql_{timestamp}.sql", "w", encoding="utf-8") as f:
    f.write(sql_total)

print("✅ Archivos SQL generados en 'parciales_sql' y uno unificado.")
</code></pre>
<hr />
<h2>🧾 6. Ejemplo de Salida Final</h2>
<p>Archivo <code>diccionario_sql_2025-10-31-15-30.sql</code>:</p>
<pre><code class="language-sql">ALTER TABLE app_cloud_billing
  MODIFY COLUMN resourceName VARCHAR(512) NULL 
    COMMENT 'Nombre único y local del recurso de Azure',
  MODIFY COLUMN tags JSON NULL 
    COMMENT 'Metadatos de facturación y atribución en formato JSON. Contiene etiquetas de clave/valor (ej. ambiente, proyecto, dueño) aplicadas al recurso';
</code></pre>
<p>Archivos parciales (por tabla):</p>
<pre><code class="language-plaintext">📁 parciales_sql/
 ┣ 📄 app_cloud_billing_2025-10-31-15-30.sql
 ┣ 📄 app_cloud_usage_2025-10-31-15-30.sql
 ┗ 📄 app_cloud_account_2025-10-31-15-30.sql
</code></pre>
<hr />
<h2>⚙️ 7. Requerimientos Python</h2>
<p>Archivo <code>requirements.txt</code> mínimo:</p>
<pre><code class="language-apache">openai
pandas
</code></pre>
<blockquote>
<p>Si no usas OpenAI directamente, sustituye por el SDK del modelo que utilices (por ejemplo <code>openfoundry</code>).</p>
</blockquote>
<hr />
<h2>🚀 Conclusión</h2>
<p>Este proceso te permite:</p>
<ul>
<li><p>Documentar automáticamente la metadata de tu base de datos.</p>
</li>
<li><p>Generar descripciones contextuales y consistentes usando IA.</p>
</li>
<li><p>Mantener tus diccionarios actualizados con control de versiones.</p>
</li>
<li><p>Aplicar los comentarios directamente en tu esquema SQL.</p>
</li>
</ul>
<p>Es una solución práctica para equipos que manejan muchos modelos de datos y necesitan <strong>mantener la documentación sincronizada con el esquema técnico</strong>.</p>
]]></content:encoded></item><item><title><![CDATA[Cómo solucionar el error de "Pending Verification" en Netlify usando Namecheap]]></title><description><![CDATA[Si estás leyendo esto, probablemente estás atrapado en el limbo de Netlify viendo el mensaje "Pending DNS verification" durante horas. Intentaste seguir la documentación oficial, añadiste el registro ]]></description><link>https://devblog.stalinmaza.com/c-mo-solucionar-el-error-de-pending-verification-en-netlify-usando-namecheap</link><guid isPermaLink="true">https://devblog.stalinmaza.com/c-mo-solucionar-el-error-de-pending-verification-en-netlify-usando-namecheap</guid><dc:creator><![CDATA[Stalin Maza]]></dc:creator><pubDate>Tue, 24 Mar 2026 17:55:06 GMT</pubDate><content:encoded><![CDATA[<p>Si estás leyendo esto, probablemente estás atrapado en el limbo de Netlify viendo el mensaje <strong>"Pending DNS verification"</strong> durante horas. Intentaste seguir la documentación oficial, añadiste el registro A con tu nombre de dominio, pero la verificación simplemente no pasa.</p>
<p>Como desarrollador, después de enfrentarme a este problema, descubrí que el conflicto suele estar en la interpretación que hace Namecheap de los valores del Host y en registros "fantasma" que vienen configurados por defecto.</p>
<p>Aquí te explico cómo solucionarlo paso a paso.</p>
<h2>El Problema: La confusión con el Host y la IP fija</h2>
<p>Netlify suele recomendar que apuntes tu dominio raíz (Apex domain) a su IP <code>75.2.60.5</code>. Sin embargo, hay dos detalles que rompen esta configuración en Namecheap:</p>
<ol>
<li><p><strong>El valor del Host:</strong> La interfaz de Netlify a veces sugiere escribir el nombre de tu dominio (ej. <a href="http://misitio.com"><code>misitio.com</code></a>) en el campo Host. En Namecheap, esto puede causar que el DNS intente resolver <a href="http://misitio.com.misitio.com"><code>misitio.com.misitio.com</code></a>.</p>
</li>
<li><p><strong>Registros AAAA:</strong> Muchos dominios nuevos en Namecheap traen registros IPv6 por defecto que entran en conflicto con la validación de Netlify.</p>
</li>
</ol>
<hr />
<h2>La Solución Técnica</h2>
<h3>1. El uso del símbolo <code>@</code></h3>
<p>En el panel <strong>Advanced DNS</strong> de Namecheap, nunca escribas tu dominio completo en la columna de Host. Debes usar el símbolo <code>@</code>. Este símbolo es un estándar en DNS que representa la raíz del dominio (el Apex).</p>
<h3>2. Optar por un Registro ALIAS (Mejor que el Registro A)</h3>
<p>Aunque Netlify te da una IP, lo ideal para aprovechar su red CDN es usar un registro de tipo <strong>ALIAS</strong>.</p>
<ul>
<li><p><strong>Host:</strong> <code>@</code></p>
</li>
<li><p><strong>Value:</strong> <a href="http://tu-app.netlify.app"><code>tu-app.netlify.app</code></a> (tu subdominio gratuito de Netlify).</p>
</li>
</ul>
<p>Esto permite que Netlify gestione el tráfico de forma dinámica. Si tu proveedor no permite ALIAS, usa el registro <strong>A</strong> apuntando a <code>75.2.60.5</code>, pero siempre con el Host en <code>@</code>.</p>
<h3>3. Limpieza de registros IPv6 (AAAA)</h3>
<p>Este es el paso que la mayoría olvida. Si haces un <code>nslookup</code> y ves direcciones largas con letras y números (IPv6), debes <strong>borrar todos los registros AAAA</strong> en Namecheap. Netlify necesita que el dominio raíz resuelva únicamente a su infraestructura para validar el SSL.</p>
<hr />
<h2>Resumen de la configuración ideal en Namecheap</h2>
<table>
<thead>
<tr>
<th><strong>Type</strong></th>
<th><strong>Host</strong></th>
<th><strong>Value</strong></th>
<th><strong>TTL</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>ALIAS Record</strong> (o A)</td>
<td><code>@</code></td>
<td><a href="http://tu-app.netlify.app"><code>tu-app.netlify.app</code></a> (o <code>75.2.60.5</code>)</td>
<td>Automatic / 1 min</td>
</tr>
<tr>
<td><strong>CNAME Record</strong></td>
<td><code>www</code></td>
<td><a href="http://tu-app.netlify.app"><code>tu-app.netlify.app</code></a></td>
<td>Automatic</td>
</tr>
</tbody></table>
<hr />
<h2>Cómo verificar que el cambio funcionó (Modo Pro)</h2>
<p>No esperes a que la interfaz de Netlify se actualice, ya que tiene caché. Abre tu terminal y usa este comando para consultar directamente a los servidores de Google:</p>
<p>Bash</p>
<pre><code class="language-python">nslookup -type=A tudominio.com 8.8.8.8
# Si la respuesta te devuelve la IP de Netlify (o el alias) y ya no aparecen direcciones IPv6,
# ve a tu panel de Netlify y presiona "Verify". La validación debería ser instantánea.
</code></pre>
<h3>Conclusión</h3>
<p>La documentación es una guía, pero cada registrador tiene sus reglas. En Namecheap, la clave es el <code>@</code> y la limpieza de registros AAAA. ¡Espero que esto te ahorre las horas de frustración que a mí me costó descubrirlo!</p>
]]></content:encoded></item><item><title><![CDATA[Debugging YAML: El misterio de los caracteres "prohibidos" en Kubernetes]]></title><description><![CDATA[¿Alguna vez tu pipeline de Azure DevOps falló en el paso de KubernetesManifest con un error críptico como expected <block end>, but found '<scalar>'?
Si usas Replace Tokens para inyectar secretos desd]]></description><link>https://devblog.stalinmaza.com/debugging-yaml-el-misterio-de-los-caracteres-prohibidos-en-kubernetes</link><guid isPermaLink="true">https://devblog.stalinmaza.com/debugging-yaml-el-misterio-de-los-caracteres-prohibidos-en-kubernetes</guid><dc:creator><![CDATA[Stalin Maza]]></dc:creator><pubDate>Tue, 24 Mar 2026 17:54:26 GMT</pubDate><content:encoded><![CDATA[<p>¿Alguna vez tu pipeline de Azure DevOps falló en el paso de <code>KubernetesManifest</code> con un error críptico como <code>expected &lt;block end&gt;, but found '&lt;scalar&gt;'</code>?</p>
<p>Si usas <strong>Replace Tokens</strong> para inyectar secretos desde tu Library, el culpable no es tu código, sino un símbolo "rebelde".</p>
<p>El error que aparece es similar a lo siguiente:</p>
<pre><code class="language-log">2026-03-24T15:49:43.6432543Z ##[section]Starting: Apply ConfigMap
2026-03-24T15:49:43.6457385Z ==============================================================================
2026-03-24T15:49:43.6457745Z Task         : Deploy to Kubernetes
2026-03-24T15:49:43.6458241Z Description  : Use Kubernetes manifest files to deploy to clusters or even bake the manifest files to be used for deployments using Helm charts
2026-03-24T15:49:43.6458631Z Version      : 1.268.0
2026-03-24T15:49:43.6458882Z Author       : Microsoft Corporation
2026-03-24T15:49:43.6459148Z Help         : https://aka.ms/azpipes-k8s-manifest-tsg
2026-03-24T15:49:43.6459451Z ==============================================================================
2026-03-24T15:49:46.4423474Z 			Kubectl Client Version: v1.35.2
2026-03-24T15:49:46.4424129Z 			Kubectl Server Version: v1.33.7
2026-03-24T15:49:46.4424737Z ==============================================================================
2026-03-24T15:49:46.4694986Z ##[error]can not read a block mapping entry; a multiline key may not be an implicit key at line 19, column 48:
     ... M_COMERCIAL_SHAREPOINT_TENANT_ID: "22cfcf07-cbdf-458d-adc0-6e57ece ... 
                                         ^
2026-03-24T15:49:46.4789324Z ##[section]Finishing: Apply ConfigMap
</code></pre>
<h2>El Problema: Caracteres con "superpoderes</h2>
<p>En YAML, símbolos como <strong>!, @, #, :, | o &gt;</strong> no son solo texto; son instrucciones técnicas.</p>
<ul>
<li><p>Ejemplo Real: Los IDs de SharePoint/Drive de Microsoft suelen empezar con b!.</p>
</li>
<li><p><a href="https://learn.microsoft.com/en-us/graph/api/drive-get?view=graph-rest-1.0&amp;tabs=http#code-try-13?view=graph-rest-1.0&amp;tabs=http">https://learn.microsoft.com/en-us/graph/api/drive-get?view=graph-rest-1.0&amp;tabs=http#code-try-13?view=graph-rest-1.0&amp;tabs=http</a></p>
</li>
<li><p>El Caos: Al inyectar b!t18... directamente en el YAML, Kubernetes intenta interpretar el <strong>!</strong> como un tag de datos y, al no entenderlo, rompe el despliegue.</p>
</li>
</ul>
<h2>La Herramienta de Diagnóstico: yamllint</h2>
<p>Antes de lanzar el despliegue y esperar 5 minutos a que falle, añade un paso de validación en tu pipeline. yamllint es el juez estricto que necesitas:</p>
<pre><code class="language-yaml">- script: |
    pip install yamllint
    # Validamos el archivo procesado por el Replace Tokens
    yamllint qa/configmap.yml
displayName: 'QA: Validar sintaxis YAML'
</code></pre>
<blockquote>
<p>Tip: Si es muy estricto con los espacios o largos de línea, usa un archivo .yamllint.conf para relajar las reglas y enfocarte solo en errores de sintaxis.</p>
</blockquote>
<p>En el caso de que la librería encuentre algún error, te mostrara una salida similar a la siguiente:</p>
<pre><code class="language-log">/home/vsts/work/1/s/qa/configmap.yml
  1:1       warning  missing document start "---"  (document-start)
  14:81     error    line too long (84 &gt; 80 characters)  (line-length)
  17:81     error    line too long (87 &gt; 80 characters)  (line-length)
  18:51     error    syntax error: expected &lt;block end&gt;, but found '&lt;scalar&gt;' (syntax)
  19:81     error    line too long (87 &gt; 80 characters)  (line-length)
  20:81     error    line too long (111 &gt; 80 characters)  (line-length)
  21:81     error    line too long (105 &gt; 80 characters)  (line-length)
  22:81     error    line too long (113 &gt; 80 characters)  (line-length)
  26:81     error    line too long (86 &gt; 80 characters)  (line-length)
</code></pre>
<h2>La Solución Definitiva: El Bloque Literal (&gt;-)</h2>
<p>Si las comillas dobles o simples fallan, usa el "arma secreta" de YAML: el Folded Block Scalar (&gt;-). Este operador le dice a Kubernetes: "Oye, todo lo que viene en la siguiente línea es texto puro. No intentes interpretarlo, no busques comandos, solo léelo como una cadena".</p>
<h3>Así debe lucir tu ConfigMap:</h3>
<pre><code class="language-yaml">data:
  # El &gt;- limpia saltos de línea y protege el valor inyectado
  SHAREPOINT_DRIVE_ID: &gt;-
    #{SHAREPOINT_DRIVE_ID}#
</code></pre>
<blockquote>
<p>Valida temprano: Usa yamllint en el pipeline para ahorrar tiempo de debugging.</p>
</blockquote>
<blockquote>
<p>Usa &gt;-: Es la forma más robusta de manejar IDs complejos y secretos con símbolos raros sin romper la estructura.</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Guía de Implementación: Bot de Conversación en Python con AI Foundry y Azure]]></title><description><![CDATA[Esta guía detalla cómo construir una arquitectura de bot escalable que conecta un backend en Python con un agente de Inteligencia Artificial (AI Foundry), lo despliega en Azure App Service y lo conecta de forma segura a Azure Bot Service utilizando e...]]></description><link>https://devblog.stalinmaza.com/guia-de-implementacion-bot-de-conversacion-en-python-con-ai-foundry-y-azure</link><guid isPermaLink="true">https://devblog.stalinmaza.com/guia-de-implementacion-bot-de-conversacion-en-python-con-ai-foundry-y-azure</guid><category><![CDATA[Azure AI Foundry]]></category><category><![CDATA[azure bot service]]></category><category><![CDATA[App Service]]></category><category><![CDATA[service principal]]></category><dc:creator><![CDATA[Stalin Maza]]></dc:creator><pubDate>Fri, 28 Nov 2025 04:35:39 GMT</pubDate><content:encoded><![CDATA[<p>Esta guía detalla cómo construir una arquitectura de bot escalable que conecta un backend en Python con un agente de Inteligencia Artificial (AI Foundry), lo despliega en Azure App Service y lo conecta de forma segura a Azure Bot Service utilizando el concepto de Service Principal.</p>
<h2 id="heading-prerrequisitos">Prerrequisitos</h2>
<p>Un agente de AI Foundry configurado y que funcione correctamente en el playground de Azure AI Foundry.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764303544108/3bad8199-903a-4a2b-b646-3a986de6cfb1.png" alt class="image--center mx-auto" /></p>
<p>Las credenciales para la conexión con el endpoint del AI Foundry.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764303623804/a94f3daf-3a9f-48a4-8e90-7414b6fb6d00.png" alt class="image--center mx-auto" /></p>
<p>Crear un recurso <strong>Bot Service</strong> para gestionar el bot y la exportación hacía los diferentes canales.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764303753903/9a54ad51-097b-4a0d-a36d-88def5e80acf.png" alt class="image--center mx-auto" /></p>
<p>Una aplicación Service Principal, que bien podrías reutilizar la aplicación generada automáticamente por el Bot Service, la cual puedes visualizarla en la pestaña <strong>Configuración</strong> del Bot Service.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764303927107/52ec5244-804e-48ad-a208-986d6a21090b.png" alt class="image--center mx-auto" /></p>
<p>La aplicación del Service Principal debe tener añadida como una URL de redirección, la URL que se conecte al Bot Service.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764304003541/a653f763-a039-4ae9-8cfb-29f99e5ea306.png" alt class="image--center mx-auto" /></p>
<p>La aplicación no requiere permisos específicos para conectarse directamente al AI Foundry. En su lugar, debe registrarse en el recurso de AI Foundry con los roles adecuados: <strong>Cognitive Services Contributor</strong> y <strong>Azure AI User</strong>. Estos permisos garantizan que la aplicación pueda interactuar correctamente con el servicio sin necesidad de configuraciones adicionales a nivel de la propia aplicación.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764304252936/11ab951b-fa45-4ed0-8338-9897b481a8e0.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-arquitectura-del-proyecto">Arquitectura del Proyecto</h2>
<p>La arquitectura se compone de tres partes principales:</p>
<ol>
<li><p><strong>Agente AI Foundry:</strong> (Externo) Provee la lógica conversacional a través de una API.</p>
</li>
<li><p><strong>Backend Python (Flask/FastAPI):</strong> Aloja el endpoint <code>/api/messages</code>, gestiona la comunicación con el Agente AI y da formato a las respuestas. Desplegado en <strong>Azure App Service</strong>.</p>
</li>
<li><p><strong>Azure Bot Service:</strong> Actúa como el puente, autenticándose con el backend y canalizando las conversaciones (Web Chat, Teams, etc.).</p>
</li>
</ol>
<h2 id="heading-configuracion-del-backend-python-y-ai-foundry">Configuración del Backend Python y AI Foundry</h2>
<h3 id="heading-11-preparacion-del-entorno-python">1.1. Preparación del Entorno Python</h3>
<p>Asegúrate de tener un entorno virtual activo y los paquetes necesarios instalados.</p>
<pre><code class="lang-apache"><span class="hljs-comment"># Core requirements</span>
<span class="hljs-attribute">flask</span>&gt;=<span class="hljs-number">2</span>.<span class="hljs-number">0</span>.<span class="hljs-number">0</span>  # Version con soporte async
<span class="hljs-attribute">python</span>-dotenv&gt;=<span class="hljs-number">0</span>.<span class="hljs-number">19</span>.<span class="hljs-number">0</span>

<span class="hljs-comment"># Azure Bot Framework</span>
<span class="hljs-attribute">botbuilder</span>-core==<span class="hljs-number">4</span>.<span class="hljs-number">17</span>.<span class="hljs-number">0</span>
<span class="hljs-attribute">botbuilder</span>-integration-aiohttp==<span class="hljs-number">4</span>.<span class="hljs-number">17</span>.<span class="hljs-number">0</span>
<span class="hljs-attribute">botbuilder</span>-schema==<span class="hljs-number">4</span>.<span class="hljs-number">17</span>.<span class="hljs-number">0</span>
<span class="hljs-attribute">botframework</span>-connector==<span class="hljs-number">4</span>.<span class="hljs-number">17</span>.<span class="hljs-number">0</span>
<span class="hljs-attribute">botframework</span>-streaming==<span class="hljs-number">4</span>.<span class="hljs-number">17</span>.<span class="hljs-number">0</span>

<span class="hljs-comment"># Azure</span>
<span class="hljs-attribute">azure</span>-ai-agents==<span class="hljs-number">1</span>.<span class="hljs-number">1</span>.<span class="hljs-number">0</span>
<span class="hljs-attribute">azure</span>-core==<span class="hljs-number">1</span>.<span class="hljs-number">36</span>.<span class="hljs-number">0</span>
<span class="hljs-attribute">azure</span>-identity==<span class="hljs-number">1</span>.<span class="hljs-number">25</span>.<span class="hljs-number">1</span>

<span class="hljs-comment"># Async support</span>
<span class="hljs-attribute">aiohttp</span>==<span class="hljs-number">3</span>.<span class="hljs-number">13</span>.<span class="hljs-number">2</span>
<span class="hljs-attribute">aiosignal</span>==<span class="hljs-number">1</span>.<span class="hljs-number">4</span>.<span class="hljs-number">0</span>
<span class="hljs-attribute">async</span>-timeout==<span class="hljs-number">5</span>.<span class="hljs-number">0</span>.<span class="hljs-number">1</span>
<span class="hljs-attribute">aiohappyeyeballs</span>==<span class="hljs-number">2</span>.<span class="hljs-number">6</span>.<span class="hljs-number">1</span>

<span class="hljs-comment"># Utils</span>
<span class="hljs-attribute">blinker</span>==<span class="hljs-number">1</span>.<span class="hljs-number">9</span>.<span class="hljs-number">0</span>
<span class="hljs-attribute">certifi</span>==<span class="hljs-number">2025</span>.<span class="hljs-number">11</span>.<span class="hljs-number">12</span>
<span class="hljs-attribute">cffi</span>==<span class="hljs-number">2</span>.<span class="hljs-number">0</span>.<span class="hljs-number">0</span>
<span class="hljs-attribute">charset</span>-normalizer==<span class="hljs-number">3</span>.<span class="hljs-number">4</span>.<span class="hljs-number">4</span>
<span class="hljs-attribute">click</span>==<span class="hljs-number">8</span>.<span class="hljs-number">1</span>.<span class="hljs-number">8</span>
<span class="hljs-attribute">colorama</span>==<span class="hljs-number">0</span>.<span class="hljs-number">4</span>.<span class="hljs-number">6</span>
<span class="hljs-attribute">cryptography</span>==<span class="hljs-number">46</span>.<span class="hljs-number">0</span>.<span class="hljs-number">3</span>
<span class="hljs-attribute">attrs</span>==<span class="hljs-number">25</span>.<span class="hljs-number">4</span>.<span class="hljs-number">0</span>
<span class="hljs-attribute">Flask</span>==<span class="hljs-number">3</span>.<span class="hljs-number">0</span>.<span class="hljs-number">0</span>
<span class="hljs-attribute">frozenlist</span>==<span class="hljs-number">1</span>.<span class="hljs-number">8</span>.<span class="hljs-number">0</span>
<span class="hljs-attribute">gunicorn</span>==<span class="hljs-number">23</span>.<span class="hljs-number">0</span>.<span class="hljs-number">0</span>
<span class="hljs-attribute">idna</span>==<span class="hljs-number">3</span>.<span class="hljs-number">11</span>
<span class="hljs-attribute">importlib_metadata</span>==<span class="hljs-number">8</span>.<span class="hljs-number">7</span>.<span class="hljs-number">0</span>
<span class="hljs-attribute">isodate</span>==<span class="hljs-number">0</span>.<span class="hljs-number">7</span>.<span class="hljs-number">2</span>
<span class="hljs-attribute">itsdangerous</span>==<span class="hljs-number">2</span>.<span class="hljs-number">2</span>.<span class="hljs-number">0</span>
<span class="hljs-attribute">Jinja2</span>==<span class="hljs-number">3</span>.<span class="hljs-number">1</span>.<span class="hljs-number">6</span>
<span class="hljs-attribute">jsonpickle</span>==<span class="hljs-number">1</span>.<span class="hljs-number">4</span>.<span class="hljs-number">2</span>
<span class="hljs-attribute">MarkupSafe</span>==<span class="hljs-number">3</span>.<span class="hljs-number">0</span>.<span class="hljs-number">3</span>
<span class="hljs-attribute">msal</span>==<span class="hljs-number">1</span>.<span class="hljs-number">34</span>.<span class="hljs-number">0</span>
<span class="hljs-attribute">msal</span>-extensions==<span class="hljs-number">1</span>.<span class="hljs-number">3</span>.<span class="hljs-number">1</span>
<span class="hljs-attribute">msrest</span>==<span class="hljs-number">0</span>.<span class="hljs-number">7</span>.<span class="hljs-number">1</span>
<span class="hljs-attribute">multidict</span>==<span class="hljs-number">6</span>.<span class="hljs-number">7</span>.<span class="hljs-number">0</span>
<span class="hljs-attribute">oauthlib</span>==<span class="hljs-number">3</span>.<span class="hljs-number">3</span>.<span class="hljs-number">1</span>
<span class="hljs-attribute">packaging</span>==<span class="hljs-number">25</span>.<span class="hljs-number">0</span>
<span class="hljs-attribute">propcache</span>==<span class="hljs-number">0</span>.<span class="hljs-number">4</span>.<span class="hljs-number">1</span>
<span class="hljs-attribute">pycparser</span>==<span class="hljs-number">2</span>.<span class="hljs-number">23</span>
<span class="hljs-attribute">PyJWT</span>==<span class="hljs-number">2</span>.<span class="hljs-number">10</span>.<span class="hljs-number">1</span>
<span class="hljs-attribute">python</span>-dotenv==<span class="hljs-number">1</span>.<span class="hljs-number">2</span>.<span class="hljs-number">1</span>
<span class="hljs-attribute">requests</span>==<span class="hljs-number">2</span>.<span class="hljs-number">32</span>.<span class="hljs-number">5</span>
<span class="hljs-attribute">requests</span>-oauthlib==<span class="hljs-number">2</span>.<span class="hljs-number">0</span>.<span class="hljs-number">0</span>
<span class="hljs-attribute">typing_extensions</span>==<span class="hljs-number">4</span>.<span class="hljs-number">15</span>.<span class="hljs-number">0</span>
<span class="hljs-attribute">urllib3</span>==<span class="hljs-number">2</span>.<span class="hljs-number">5</span>.<span class="hljs-number">0</span>
<span class="hljs-attribute">Werkzeug</span>==<span class="hljs-number">3</span>.<span class="hljs-number">0</span>.<span class="hljs-number">1</span>
<span class="hljs-attribute">yarl</span>==<span class="hljs-number">1</span>.<span class="hljs-number">22</span>.<span class="hljs-number">0</span>
<span class="hljs-attribute">zipp</span>==<span class="hljs-number">3</span>.<span class="hljs-number">23</span>.<span class="hljs-number">0</span>
</code></pre>
<h2 id="heading-configuracion-del-archivo-apppy">Configuración del archivo <code>app.py</code></h2>
<p>En el archivo <code>app.py</code>, hay que configurar lo siguiente:</p>
<p><strong>Leer configuración del Bot Service</strong></p>
<pre><code class="lang-apache"><span class="hljs-attribute">APP_ID</span> = os.environ.get(<span class="hljs-string">"MicrosoftAppId"</span>, <span class="hljs-string">""</span>)
<span class="hljs-attribute">APP_PASSWORD</span> = os.environ.get(<span class="hljs-string">"MicrosoftAppPassword"</span>, <span class="hljs-string">""</span>)
<span class="hljs-attribute">APP_TENANT_ID</span> = os.environ.get(<span class="hljs-string">"MicrosoftAppTenantId"</span>, <span class="hljs-string">""</span>)
<span class="hljs-attribute">SETTINGS</span> = BotFrameworkAdapterSettings(APP_ID, APP_PASSWORD, APP_TENANT_ID)
<span class="hljs-attribute">ADAPTER</span> = BotFrameworkAdapter(SETTINGS)
<span class="hljs-attribute">BOT</span> = FoundryAgentBot()
</code></pre>
<p><strong>Inicializar la aplicación de Flask</strong></p>
<pre><code class="lang-python">app = Flask(__name__)
app.config[<span class="hljs-string">'JSON_AS_ASCII'</span>] = <span class="hljs-literal">False</span>
</code></pre>
<p><strong>Crear el endpoint de Health Check</strong></p>
<p>Permite verificar al momento del despliegue si el servicio fue desplegado correctamente.</p>
<pre><code class="lang-python"><span class="hljs-meta">@app.route("/", methods=["GET"])</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">health_check</span>():</span>
    <span class="hljs-string">"""
    Endpoint de health check que devuelve el estado del servicio.
    """</span>
    <span class="hljs-keyword">return</span> Response(<span class="hljs-string">"Bot Service is Running and Healthy"</span>, status=<span class="hljs-number">200</span>, mimetype=<span class="hljs-string">"text/plain"</span>)
</code></pre>
<p><strong>Crear el endpoint que se comunica con el BotService</strong></p>
<p>El estándar de la ruta es <strong>api/messages</strong>, recibe una petición HTTP de tipo JSON y que se autentica mediante un token de autorización <strong>Bearer</strong>.</p>
<pre><code class="lang-python"><span class="hljs-meta">@app.route("/api/messages", methods=["POST"])</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">messages</span>():</span>
    <span class="hljs-keyword">if</span> <span class="hljs-string">"application/json"</span> <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> request.headers.get(<span class="hljs-string">"Content-Type"</span>, <span class="hljs-string">""</span>):
        <span class="hljs-keyword">return</span> Response(<span class="hljs-string">"Unsupported Media Type"</span>, status=<span class="hljs-number">415</span>)

    <span class="hljs-keyword">try</span>:
        <span class="hljs-comment"># Obtener el body y el header de autorización</span>
        body = request.json
        auth_header = request.headers.get(<span class="hljs-string">"Authorization"</span>, <span class="hljs-string">""</span>)
        <span class="hljs-comment"># Deserializar la actividad</span>
        activity = Activity.deserialize(body)

        <span class="hljs-comment"># Procesar la actividad de forma asíncrona</span>
        <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">process</span>():</span>
            <span class="hljs-keyword">await</span> ADAPTER.process_activity(
                activity, 
                auth_header, 
                <span class="hljs-keyword">lambda</span> context: BOT.on_turn(context)
            )

        <span class="hljs-comment"># Ejecutar la función async (asyncio.run crea un nuevo event loop)</span>
        asyncio.run(process())
        <span class="hljs-keyword">return</span> Response(status=<span class="hljs-number">200</span>)

    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        logging.error(<span class="hljs-string">f"Error procesando la actividad: <span class="hljs-subst">{e}</span>"</span>)
        <span class="hljs-keyword">return</span> Response(str(e), status=<span class="hljs-number">500</span>, mimetype=<span class="hljs-string">"text/plain"</span>)
</code></pre>
<p><strong>Iniciar la aplicación Flask</strong></p>
<pre><code class="lang-python"><span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">"__main__"</span>:
    port = int(os.environ.get(<span class="hljs-string">"PORT"</span>, <span class="hljs-number">5000</span>))
    host = os.environ.get(<span class="hljs-string">"HOST"</span>, <span class="hljs-string">"0.0.0.0"</span>)
    app.run(host=host, port=port, debug=<span class="hljs-literal">True</span>)
</code></pre>
<h2 id="heading-configuracion-del-archivo-foundrypy">Configuración del archivo <code>foundry.py</code></h2>
<p><strong>Leer las configuraciones del AI Foundry</strong></p>
<pre><code class="lang-python">PROJECT_ENDPOINT = os.environ.get(<span class="hljs-string">"AZURE_AI_FOUNDRY_PROJECT_ENDPOINT"</span>)
AGENT_ID = os.environ.get(<span class="hljs-string">"AZURE_AI_FOUNDRY_AGENT_ID"</span>)
</code></pre>
<p><strong>Inicializar la clase que gestiona la conexión al Foundry</strong></p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FoundryAgentBot</span>(<span class="hljs-params">ActivityHandler</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-comment"># Autenticación: DefaultAzureCredential busca credenciales en el entorno</span>
        self.credential = DefaultAzureCredential()
        <span class="hljs-comment"># Cliente para interactuar con los servicios de Agentes de Foundry</span>
        self.agent_client = AgentsClient(endpoint=PROJECT_ENDPOINT, credential=self.credential)
        <span class="hljs-comment"># Diccionario para almacenar el Thread ID de Foundry por usuario/conversación</span>
        self.user_threads = {}
</code></pre>
<p><strong>Añadir el método para gestionar la actividad de los mensajes</strong></p>
<p>Primero hay que definir la función junto con los parámetros que requiere.</p>
<pre><code class="lang-python"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_message_activity</span>(<span class="hljs-params">self, turn_context: TurnContext</span>):</span>
    <span class="hljs-string">"""Maneja el mensaje de entrada del usuario."""</span>
</code></pre>
<p>Obtener el contexto de Foundry y crear un hilo de conversación</p>
<pre><code class="lang-python"><span class="hljs-comment"># Obtener el ID de la conversación</span>
conversation_id = turn_context.activity.conversation.id
<span class="hljs-comment"># Generar un identificador para el hilo de la conversación</span>
thread_id = self.user_threads.get(conversation_id)

<span class="hljs-comment"># Crear un nuevo hilo en caso no exista uno</span>
<span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> thread_id:
    new_thread = self.agent_client.threads.create()
    thread_id = new_thread.id
    self.user_threads[conversation_id] = thread_id
    <span class="hljs-keyword">await</span> turn_context.send_activity(
        <span class="hljs-string">"¡Hola! Soy un agente impulsado por Azure AI Foundry. ¿En qué puedo ayudarte?"</span>
    )

<span class="hljs-comment"># Enviar el mensaje del usuario como actividad al agente de Foundry</span>
user_message = turn_context.activity.text
<span class="hljs-keyword">await</span> turn_context.send_activity(<span class="hljs-string">f"Enviando el mensaje del usuario hacía AI Foundry...** '<span class="hljs-subst">{user_message}</span>'"</span>)

<span class="hljs-comment"># Crear el cliente del agente con el contexto, el rol 'Usuario' y el identificador del Thread.</span>
<span class="hljs-keyword">try</span>:
    self.agent_client.messages.create(
        thread_id=thread_id,
        role=<span class="hljs-string">"user"</span>,
        content=user_message
    )
<span class="hljs-keyword">except</span> AttributeError <span class="hljs-keyword">as</span> e:
    <span class="hljs-keyword">raise</span> Exception(<span class="hljs-string">f"Error al crear mensaje: <span class="hljs-subst">{e}</span>. Revisar sintaxis de create_message."</span>)
</code></pre>
<p><strong>Implementar un bucle de polling para la conexión al momento de obtener los mensajes</strong></p>
<pre><code class="lang-python"> <span class="hljs-comment"># Implementar el bucle de POLLING (Sondeo) manual</span>
<span class="hljs-keyword">while</span> run.status <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> [<span class="hljs-string">"completed"</span>, <span class="hljs-string">"failed"</span>, <span class="hljs-string">"cancelled"</span>, <span class="hljs-string">"expired"</span>]:
    <span class="hljs-comment"># Esperar 1 segundo antes de verificar el estado nuevamente</span>
    <span class="hljs-keyword">await</span> asyncio.sleep(<span class="hljs-number">1</span>) 

    <span class="hljs-comment"># Obtener el estado actualizado del run usando el método 'get' del objeto 'runs'</span>
    run = self.agent_client.runs.get(
        thread_id=thread_id,
        run_id=run.id
    )
</code></pre>
<p><strong>Obtener la respuesta del agente</strong></p>
<pre><code class="lang-python"><span class="hljs-keyword">if</span> run.status == <span class="hljs-string">"completed"</span>:
    messages_result = self.agent_client.messages.list( 
        thread_id=thread_id, 
        order=<span class="hljs-string">"desc"</span>, 
        limit=<span class="hljs-number">1</span>
    )
    messages_list = list(messages_result) 
    <span class="hljs-comment"># Obtener el contenido del mensaje</span>
    <span class="hljs-keyword">if</span> messages_list <span class="hljs-keyword">and</span> messages_list[<span class="hljs-number">0</span>] <span class="hljs-keyword">and</span> messages_list[<span class="hljs-number">0</span>].content:
        result_message = messages_list[<span class="hljs-number">0</span>].content[<span class="hljs-number">0</span>]
        result_response = result_message[<span class="hljs-string">'text'</span>][<span class="hljs-string">'value'</span>]
        agent_response = result_response
    <span class="hljs-keyword">else</span>:
        agent_response = <span class="hljs-string">"No se recibió respuesta del agente."</span>
<span class="hljs-keyword">else</span>:
    agent_response = <span class="hljs-string">f"El agente no pudo completar la tarea. Estado: <span class="hljs-subst">{run.status}</span>. Error: <span class="hljs-subst">{run.last_error}</span>"</span>
</code></pre>
<p><strong>Responder al usuario con el mensaje que retorna el agente de AI Foundry</strong></p>
<pre><code class="lang-python"><span class="hljs-keyword">await</span> turn_context.send_activity(agent_response)
</code></pre>
<p>Una vez desplegado el proyecto en un APP Service, se debe configurar las variables de entorno, una configuración similar a la siguiente.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764304354925/7e889a10-6c81-4829-be45-84984675e4d9.png" alt class="image--center mx-auto" /></p>
<p>Y finalmente en el Bot Service accediendo al apartado de Canales, se puede probar utilizando el canal web que es la forma más rápida.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764304440279/6549aa42-dbf7-44a0-b867-f570a55914b9.png" alt class="image--center mx-auto" /></p>
<p><strong>Bonus</strong></p>
<p>El siguiente código permite manejar el evento cuando el Bot se despliegue en un entorno como Microsoft Teams mediante los canales del Bot Service.</p>
<pre><code class="lang-python"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_members_added_activity</span>(<span class="hljs-params">self, members_added: list[ChannelAccount], turn_context: TurnContext</span>):</span>
    <span class="hljs-string">"Maneja cuando el bot es añadido a una conversación (útil en Teams)."</span>
    <span class="hljs-keyword">for</span> member <span class="hljs-keyword">in</span> members_added:
        <span class="hljs-keyword">if</span> member.id != turn_context.activity.recipient.id:
            <span class="hljs-keyword">await</span> turn_context.send_activity(
                Activity(
                    type=ActivityTypes.message, 
                    text=<span class="hljs-string">"¡Hola! Soy un bot que usa un Agente de Azure AI Foundry. Puedes empezar a chatear conmigo."</span>
                )
            )
</code></pre>
<p>Además si necesitas probar generando tu propio token puedes hacerlo mediante este pequeño script de Python.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Importar librerias</span>
<span class="hljs-keyword">from</span> azure.identity <span class="hljs-keyword">import</span> DefaultAzureCredential
<span class="hljs-keyword">from</span> datetime <span class="hljs-keyword">import</span> datetime

<span class="hljs-comment"># Inicializar credencial, utilizará los valores dependiendo del entorno que tengas, sea proporcionando</span>
<span class="hljs-comment"># credenciales de Service Principal o de una identidad gestionada del sistema</span>
credential = DefaultAzureCredential()

<span class="hljs-comment"># Scope para Microsoft Graph (puedes cambiarlo según tu necesidad)</span>
scope = <span class="hljs-string">"https://graph.microsoft.com/.default"</span>

<span class="hljs-comment"># Obtener token</span>
token = credential.get_token(scope)
expires_readable = datetime.utcfromtimestamp(token.expires_on).strftime(<span class="hljs-string">'%Y-%m-%d %H:%M:%S'</span>)
print(<span class="hljs-string">"Access Token:"</span>, token.token)
print(<span class="hljs-string">"Expires On:"</span>, token.expires_on)
print(<span class="hljs-string">"Expires On expires_readable:"</span>, expires_readable)
</code></pre>
<p>Por último si deseas probar el chat mediante un endpoint HTTP, puedes hacerlo mediante el siguiente código que añade una ruta que la puedes probar desde un cliente como <strong>Postman</strong>, <strong>Insomnia</strong> o un Frontend.</p>
<pre><code class="lang-python"><span class="hljs-meta">@app.route("/api/chat", methods=["POST"])</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">chat</span>():</span>
    <span class="hljs-string">"""
    Endpoint HTTP simple para probar el bot con peticiones REST estándar.
    Acepta un JSON con un campo 'message' y devuelve la respuesta del bot.
    """</span>
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> request.is_json:
        <span class="hljs-keyword">return</span> jsonify({<span class="hljs-string">"error"</span>: <span class="hljs-string">"Content type must be application/json"</span>}), <span class="hljs-number">415</span>

    <span class="hljs-keyword">try</span>:
        data = request.get_json()
        user_message = data.get(<span class="hljs-string">'message'</span>, <span class="hljs-string">''</span>)

        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> user_message:
            <span class="hljs-keyword">return</span> jsonify({<span class="hljs-string">"error"</span>: <span class="hljs-string">"Field 'message' is required"</span>}), <span class="hljs-number">400</span>

        <span class="hljs-comment"># Crear una actividad de mensaje simple con todos los campos requeridos</span>
        <span class="hljs-keyword">from</span> botbuilder.schema <span class="hljs-keyword">import</span> ConversationAccount
        <span class="hljs-keyword">from</span> botbuilder.core <span class="hljs-keyword">import</span> TurnContext, BotAdapter

        <span class="hljs-comment"># Crear la actividad con los campos mínimos requeridos</span>
        activity = Activity(
            type=<span class="hljs-string">"message"</span>,
            text=user_message,
            channel_id=<span class="hljs-string">"http"</span>,
            from_property={<span class="hljs-string">"id"</span>: <span class="hljs-string">"http-user"</span>, <span class="hljs-string">"name"</span>: <span class="hljs-string">"HTTP User"</span>, <span class="hljs-string">"role"</span>: <span class="hljs-string">"user"</span>},
            recipient={<span class="hljs-string">"id"</span>: <span class="hljs-string">"bot"</span>, <span class="hljs-string">"role"</span>: <span class="hljs-string">"bot"</span>},
            conversation=ConversationAccount(
                id=<span class="hljs-string">"http-conversation-123"</span>,  <span class="hljs-comment"># ID de conversación fijo para pruebas</span>
                name=<span class="hljs-string">"Test Conversation"</span>
            ),
            service_url=<span class="hljs-string">"http://localhost:5000"</span>, <span class="hljs-comment"># Reemplazar con la URL del servicio si es necesario cuando se use en producción</span>
            channel_data={},
            reply_to_id=<span class="hljs-string">""</span>
        )

        <span class="hljs-comment"># Variable para almacenar la respuesta</span>
        response_text = <span class="hljs-string">"No se recibió respuesta del bot"</span>

        <span class="hljs-comment"># Crear un adaptador simple para el contexto</span>
        <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SimpleAdapter</span>(<span class="hljs-params">BotAdapter</span>):</span>
            <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self</span>):</span>
                super().__init__()
                self.sent_messages = []

            <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">send_activities</span>(<span class="hljs-params">self, context, activities</span>):</span>
                responses = []
                <span class="hljs-keyword">for</span> activity <span class="hljs-keyword">in</span> activities:
                    self.sent_messages.append(activity)
                    responses.append({<span class="hljs-string">'id'</span>: <span class="hljs-string">'123'</span>})
                <span class="hljs-keyword">return</span> responses

            <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">delete_activity</span>(<span class="hljs-params">self, context, reference</span>):</span>
                <span class="hljs-string">"""Delete an existing activity."""</span>
                <span class="hljs-comment"># Implementación básica para cumplir con la interfaz</span>
                <span class="hljs-keyword">return</span>

            <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">update_activity</span>(<span class="hljs-params">self, context, activity</span>):</span>
                <span class="hljs-string">"""Update an existing activity."""</span>
                <span class="hljs-comment"># Implementación básica para cumplir con la interfaz</span>
                <span class="hljs-keyword">return</span> {<span class="hljs-string">'id'</span>: <span class="hljs-string">'123'</span>}

        <span class="hljs-comment"># Crear el contexto de turno</span>
        adapter = SimpleAdapter()
        context = TurnContext(adapter, activity)
        context.turn_state[<span class="hljs-string">'BotCallbackHandler'</span>] = <span class="hljs-literal">None</span>

        <span class="hljs-comment"># Procesar la actividad de forma síncrona</span>
        <span class="hljs-keyword">import</span> asyncio

        <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">process</span>():</span>
            <span class="hljs-keyword">await</span> BOT.on_turn(context)

            <span class="hljs-comment"># Obtener la respuesta del bot</span>
            <span class="hljs-keyword">if</span> hasattr(adapter, <span class="hljs-string">'sent_messages'</span>) <span class="hljs-keyword">and</span> adapter.sent_messages:
                last_message = adapter.sent_messages[<span class="hljs-number">-1</span>]
                <span class="hljs-keyword">if</span> hasattr(last_message, <span class="hljs-string">'text'</span>):
                    <span class="hljs-keyword">return</span> last_message.text
                <span class="hljs-keyword">elif</span> isinstance(last_message, dict) <span class="hljs-keyword">and</span> <span class="hljs-string">'text'</span> <span class="hljs-keyword">in</span> last_message:
                    <span class="hljs-keyword">return</span> last_message[<span class="hljs-string">'text'</span>]
                <span class="hljs-keyword">return</span> str(last_message)
            <span class="hljs-keyword">return</span> <span class="hljs-string">"No se recibió respuesta del bot"</span>

        <span class="hljs-comment"># Ejecutar la corutina y esperar el resultado</span>
        response_text = asyncio.run(process())

        <span class="hljs-comment"># Devolver la respuesta en formato JSON</span>
        <span class="hljs-keyword">return</span> jsonify({<span class="hljs-string">"response"</span>: response_text})

    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        logging.error(<span class="hljs-string">f"Error en /api/chat: <span class="hljs-subst">{str(e)}</span>"</span>, exc_info=<span class="hljs-literal">True</span>)
        <span class="hljs-keyword">return</span> jsonify({<span class="hljs-string">"error"</span>: str(e)}), <span class="hljs-number">500</span>
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Cómo crear y consumir un agente en Azure Foundry]]></title><description><![CDATA[En este tutorial aprenderás a:
Crear un agente en Azure Foundry usando un modelo de OpenAI. Configurar permisos y autenticación con Service Principal. Consumir el agente desde un script Node.js. Integrarlo en un frontend simple (Angular) para crear u...]]></description><link>https://devblog.stalinmaza.com/como-crear-y-consumir-un-agente-en-azure-foundry</link><guid isPermaLink="true">https://devblog.stalinmaza.com/como-crear-y-consumir-un-agente-en-azure-foundry</guid><category><![CDATA[Service Principal - EntraId]]></category><category><![CDATA[Angular]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[Entra ID]]></category><category><![CDATA[Azure AI Foundry]]></category><dc:creator><![CDATA[Stalin Maza]]></dc:creator><pubDate>Tue, 11 Nov 2025 15:10:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1762915806991/b3f8d0c4-90b4-43c6-9215-8f79e95b4699.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>En este tutorial aprenderás a:</p>
<p>Crear un agente en Azure Foundry usando un modelo de OpenAI. Configurar permisos y autenticación con Service Principal. Consumir el agente desde un script Node.js. Integrarlo en un frontend simple (Angular) para crear un chat funcional.</p>
<h2 id="heading-crear-el-agente-en-azure-foundry"><strong>Crear el agente en Azure Foundry</strong></h2>
<p>Se debe acceder al portal de <strong>Azure Foundry</strong>. Posteriormente se debe navegar al apartado de <strong>Agents</strong> y seleccionar <strong>Create Agent</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762871421403/7f3b11f1-d783-4e4b-8a2c-474c6cc123b0.png" alt class="image--center mx-auto" /></p>
<p>Una vez en este apartado, se debe elegir un <strong>deployment de OpenAI</strong> (Azure Foundry actualmente soporta modelos de OpenAI como GPT-4, GPT-3.5, etc.).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762871619031/624e4c86-f918-4720-8a9b-fbe93fd52701.png" alt class="image--center mx-auto" /></p>
<p>En la pantalla se debe configurar lo siguiente:</p>
<ul>
<li><p><strong>Nombre del agente</strong>.</p>
</li>
<li><p><strong>Prompt inicial</strong> (instrucciones del comportamiento).</p>
</li>
<li><p><strong>Knowledge base</strong> (sube tus PDFs o documentos relevantes).</p>
<ul>
<li>Los documentos que se suben se vectorizan mediante Azure AI Search para la optimización de las búsquedas en la base de conocimientos.</li>
</ul>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762871808808/eeb86308-6927-47a0-b784-477303b1eb0f.png" alt class="image--center mx-auto" /></p>
<p>Finalmente hay que guardar los cambios y esperar a que el agente esté listo.</p>
<h2 id="heading-configurar-autenticacion-con-service-principal">Configurar autenticación con Service Principal</h2>
<p>Para consumir el agente desde tu aplicación, necesitas un Service Principal, por lo cual debes navegar hacia el apartado de Azure Active Directory → App Registrations → New Registration.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762872153197/0a09b95a-992b-4f92-80a6-5f10d48025fb.png" alt class="image--center mx-auto" /></p>
<p>Entonces se debe asignar un nombre ej. <code>foundry-agent-client</code>). Una vez creado se debe copiar el valor del <strong>Application (client) ID</strong> y el <strong>Directory (tenant) ID</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762872437654/0c348440-a654-44f1-89c5-f226f71c587d.png" alt class="image--center mx-auto" /></p>
<p>Posteriormente se debe generar un Client Secret, para esto se debe navegar a la sección de Certificates &amp; Secrets → New Client Secret. Guarda el valor en un lugar seguro (lo usarás en tu script) ya que este valor no aparecerá nuevamente.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762872579586/a2f26dd6-f5b5-4b48-9d52-08d8ac8f8ce0.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-asignar-permisos-al-service-principal">Asignar permisos al Service Principal</h2>
<p>Ve a Azure Foundry → IAM (Access Control). En este apartado añade el Service Principal como Cognitive Services Contributor. Esto le permitirá interactuar con el agente vía API.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762872724728/f2ff3770-c8cf-49a1-b5bb-e54d242bdc7e.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-consumir-el-agente-desde-nodejs">Consumir el agente desde Node.js</h2>
<p>Crea un pequeño script para conectarte al agente, para esto crea un proyecto de Node.js (Solo para el ejemplo, se puede usar también Python o .NET).</p>
<p>Primero debes instalar las siguientes dependencias:</p>
<pre><code class="lang-bash">npm install @azure/ai-projects @azure/identity body-parser dotenv express
</code></pre>
<p>Posteriormente se debe crear el archivo <strong>.env</strong> con los siguientes valores:</p>
<pre><code class="lang-bash">
AZURE_TENANT_ID=&lt;TENAND_ID&gt;
AZURE_CLIENT_SECRET=&lt;CLIENT_SECRET&gt;
AZURE_CLIENT_ID=&lt;CLIENT_ID&gt;
PROJECT_URL=
AGENT_ID=
</code></pre>
<p>Finalmente se debe ejecutar el código para consumir el proyecto, en el cual</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Importar dependencias</span>
<span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">"express"</span>);
<span class="hljs-keyword">const</span> bodyParser = <span class="hljs-built_in">require</span>(<span class="hljs-string">"body-parser"</span>);

<span class="hljs-comment">// Importar variables de entorno desde archivo .env</span>
<span class="hljs-built_in">require</span>(<span class="hljs-string">'dotenv'</span>).config()

<span class="hljs-comment">// Importaciones del SDK de Agentes de Azure AI</span>
<span class="hljs-keyword">const</span> { AIProjectClient } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"@azure/ai-projects"</span>);
<span class="hljs-keyword">const</span> { DefaultAzureCredential } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"@azure/identity"</span>);

<span class="hljs-keyword">const</span> app = express();
<span class="hljs-keyword">const</span> PORT = <span class="hljs-number">3000</span>;

<span class="hljs-comment">// *** Configuración específica de tu Agente de Azure AI Studio ***</span>
<span class="hljs-keyword">const</span> AZURE_AI_PROJECT_URL = process.env.PROJECT_URL;
<span class="hljs-keyword">const</span> AGENT_ID = process.env.AGENT_ID;

<span class="hljs-comment">// Inicializar el cliente del proyecto (se autentica usando az login en desarrollo)</span>
<span class="hljs-keyword">const</span> aiProjectClient = <span class="hljs-keyword">new</span> AIProjectClient(
  AZURE_AI_PROJECT_URL,
  <span class="hljs-keyword">new</span> DefaultAzureCredential() <span class="hljs-comment">//Esta credencial por defecto busca el tenant, clientId y clientSecret</span>
);

<span class="hljs-comment">// Aplicar middleware de body parser</span>
app.use(bodyParser.json());

<span class="hljs-comment">// Configuración de CORS para permitir la comunicación con tu Frontend (Angular)</span>
app.use(<span class="hljs-function">(<span class="hljs-params">req, res, next</span>) =&gt;</span> {
  <span class="hljs-comment">// CAMBIA ESTO por la URL de tu frontend (ej: 'http://localhost:4200')</span>
  res.header(<span class="hljs-string">"Access-Control-Allow-Origin"</span>, <span class="hljs-string">"*"</span>);
  res.header(<span class="hljs-string">"Access-Control-Allow-Headers"</span>, <span class="hljs-string">"Content-Type"</span>);
  res.header(<span class="hljs-string">'Access-Control-Allow-Methods'</span>, <span class="hljs-string">'POST, GET, OPTIONS'</span>);
  next();
});

<span class="hljs-comment">// Endpoint principal para el chat</span>
app.post(<span class="hljs-string">"/api/chat"</span>, <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">const</span> userMessage = req.body.message;

  <span class="hljs-keyword">if</span> (!userMessage) {
    <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({ <span class="hljs-attr">error</span>: <span class="hljs-string">"Falta el mensaje del usuario."</span> });
  }

  <span class="hljs-keyword">try</span> {
    <span class="hljs-comment">// 1. Crear un nuevo Hilo (Thread) para la conversación</span>
    <span class="hljs-keyword">const</span> thread = <span class="hljs-keyword">await</span> aiProjectClient.agents.threads.create();

    <span class="hljs-comment">// 2. Enviar el mensaje del usuario al Hilo</span>
    <span class="hljs-keyword">await</span> aiProjectClient.agents.messages.create(
      thread.id,
      <span class="hljs-string">"user"</span>,
      userMessage
    );

    <span class="hljs-comment">// 3. Iniciar la Ejecución (Run) del Agente sobre el Hilo</span>
    <span class="hljs-keyword">let</span> run = <span class="hljs-keyword">await</span> aiProjectClient.agents.runs.create(thread.id, AGENT_ID);

    <span class="hljs-comment">// 4. Monitorear el estado del Run hasta que termine</span>
    <span class="hljs-keyword">while</span> (run.status === <span class="hljs-string">"queued"</span> || run.status === <span class="hljs-string">"in_progress"</span>) {
      <span class="hljs-comment">// Esperar 1 segundo antes de volver a consultar el estado</span>
      <span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve</span>) =&gt;</span> <span class="hljs-built_in">setTimeout</span>(resolve, <span class="hljs-number">1000</span>));
      run = <span class="hljs-keyword">await</span> aiProjectClient.agents.runs.get(thread.id, run.id);
    }

    <span class="hljs-keyword">if</span> (run.status === <span class="hljs-string">"failed"</span>) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Run failed:"</span>, run.lastError);
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">500</span>).json({
        <span class="hljs-attr">error</span>: <span class="hljs-string">"La ejecución del Agente falló."</span>,
        <span class="hljs-attr">details</span>: run.lastError,
      });
    }

    <span class="hljs-comment">// 5. Recuperar todos los mensajes después de que el Agente ha respondido</span>
    <span class="hljs-keyword">const</span> messagesIterator = aiProjectClient.agents.messages.list(thread.id, {
      <span class="hljs-attr">order</span>: <span class="hljs-string">"desc"</span>,
    });

    <span class="hljs-keyword">const</span> allMessages = [];
    <span class="hljs-keyword">for</span> <span class="hljs-keyword">await</span> (<span class="hljs-keyword">const</span> m <span class="hljs-keyword">of</span> messagesIterator) {
      allMessages.push(m);
    }

    <span class="hljs-comment">// Ahora, puedes usar .find() en el array allMessages</span>
    <span class="hljs-keyword">const</span> agentResponseMsg = allMessages.find(<span class="hljs-function">(<span class="hljs-params">m</span>) =&gt;</span> m.role === <span class="hljs-string">"assistant"</span>);

    <span class="hljs-keyword">let</span> agentReplyText = <span class="hljs-string">"Lo siento, no pude obtener una respuesta válida."</span>;

    <span class="hljs-keyword">if</span> (agentResponseMsg) {
      <span class="hljs-comment">// ... el resto de tu lógica para extraer el texto ...</span>
      <span class="hljs-keyword">const</span> content = agentResponseMsg.content.find(
        <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> c.type === <span class="hljs-string">"text"</span> &amp;&amp; <span class="hljs-string">"text"</span> <span class="hljs-keyword">in</span> c
      );
      <span class="hljs-keyword">if</span> (content) {
        agentReplyText = content.text.value;
      }
    }
    <span class="hljs-comment">// 6. Retornar la respuesta al Frontend</span>
    res.json({ <span class="hljs-attr">reply</span>: agentReplyText });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error al comunicarse con Azure AI Agents:"</span>, error);
    res
      .status(<span class="hljs-number">500</span>)
      .json({ <span class="hljs-attr">error</span>: <span class="hljs-string">"Error interno del servidor al procesar la solicitud."</span> });
  }
});

app.listen(PORT, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Servidor intermedio escuchando en http://localhost:<span class="hljs-subst">${PORT}</span>`</span>);
});
</code></pre>
<h2 id="heading-crear-un-frontend-simple-angular">Crear un frontend simple (Angular)</h2>
<p>En este caso la aplicación será en Angular, pero puede ser cualquier frontend como React, Vue.js, etc. Para el ejemplo, se debe crear primero un servicio que se comunique con el backend.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { Injectable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { HttpClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/common/http'</span>;
<span class="hljs-keyword">import</span> { Observable } <span class="hljs-keyword">from</span> <span class="hljs-string">'rxjs'</span>;

<span class="hljs-comment">// Define la estructura de la respuesta</span>
<span class="hljs-keyword">export</span> interface ChatResponse {
  <span class="hljs-attr">reply</span>: string;
}

@Injectable({
  <span class="hljs-attr">providedIn</span>: <span class="hljs-string">'root'</span>
})
<span class="hljs-keyword">export</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ChatService</span> </span>{
  private apiUrl = <span class="hljs-string">'http://localhost:3000/api/chat'</span>; <span class="hljs-comment">// URL de tu backend</span>

  <span class="hljs-keyword">constructor</span>(private http: HttpClient) { }

  sendMessage(message: string): Observable&lt;ChatResponse&gt; {
    <span class="hljs-comment">// Envía el mensaje al backend intermediario</span>
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.http.post&lt;ChatResponse&gt;(<span class="hljs-built_in">this</span>.apiUrl, { <span class="hljs-attr">message</span>: message });
  }
}
</code></pre>
<p>Luego se debe construir una pantalla básica para visualizar el resultado del chat. En este ejemplo se usa Bootstrap y CSS.</p>
<p>Se utilizarán los siguientes estilos:</p>
<pre><code class="lang-css"><span class="hljs-comment">/* Limita el alto de la ventana de chat y habilita el scroll */</span>
<span class="hljs-selector-class">.chat-window</span> {
    <span class="hljs-attribute">height</span>: <span class="hljs-number">400px</span>;
    <span class="hljs-attribute">overflow-y</span>: auto;
    <span class="hljs-attribute">padding-bottom</span>: <span class="hljs-number">10px</span>;
    <span class="hljs-attribute">background-color</span>: <span class="hljs-number">#f8f9fa</span>; <span class="hljs-comment">/* Gris muy claro */</span>
}

<span class="hljs-comment">/* Estilo de la burbuja: limita el ancho y añade esquinas */</span>
<span class="hljs-selector-class">.message-bubble</span> {
    <span class="hljs-attribute">max-width</span>: <span class="hljs-number">70%</span>; <span class="hljs-comment">/* Las burbujas no ocupan todo el ancho */</span>
    <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">1.25rem</span>; <span class="hljs-comment">/* Más redondeado que el card default */</span>
    <span class="hljs-attribute">border</span>: none;
    <span class="hljs-attribute">box-shadow</span>: <span class="hljs-number">0</span> <span class="hljs-number">1px</span> <span class="hljs-number">2px</span> <span class="hljs-built_in">rgba</span>(<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,<span class="hljs-number">0</span>,.<span class="hljs-number">1</span>);
}

<span class="hljs-comment">/* Mensajes del Agente: alineación a la izquierda (default) */</span>
<span class="hljs-selector-class">.bg-light</span><span class="hljs-selector-class">.message-bubble</span> {
    <span class="hljs-comment">/* Ajustes para el lado del agente */</span>
    <span class="hljs-attribute">border-top-left-radius</span>: <span class="hljs-number">0.25rem</span>; <span class="hljs-comment">/* Esquina superior izquierda menos redondeada para simular el chat */</span>
}

<span class="hljs-comment">/* Mensajes del Usuario: alineación a la derecha */</span>
<span class="hljs-selector-class">.bg-primary</span><span class="hljs-selector-class">.message-bubble</span> {
    <span class="hljs-comment">/* Ajustes para el lado del usuario */</span>
    <span class="hljs-attribute">border-top-right-radius</span>: <span class="hljs-number">0.25rem</span>; <span class="hljs-comment">/* Esquina superior derecha menos redondeada */</span>
    <span class="hljs-attribute">margin-left</span>: auto; <span class="hljs-comment">/* Para asegurar el espaciado correcto */</span>
}
</code></pre>
<p>Posteriormente, en el archivo Typescript se debe declarar los valores necesarios para consumir los datos.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Component, OnInit } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { ChatService } <span class="hljs-keyword">from</span> <span class="hljs-string">'src/app/services/chat.service'</span>;


<span class="hljs-keyword">interface</span> Message {
  text: <span class="hljs-built_in">string</span>;
  sender: <span class="hljs-string">'user'</span> | <span class="hljs-string">'agent'</span>;
}
<span class="hljs-meta">@Component</span>({
  selector: <span class="hljs-string">'app-chatbot-ai'</span>,
  templateUrl: <span class="hljs-string">'./chatbot-ai.component.html'</span>,
  styleUrls: [<span class="hljs-string">'./chatbot-ai.component.scss'</span>]
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> ChatbotAiComponent <span class="hljs-keyword">implements</span> OnInit {
    messages: Message[] = [];
    currentMessage: <span class="hljs-built_in">string</span> = <span class="hljs-string">''</span>;

    <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> chatService: ChatService</span>) {}

    sendMessage(): <span class="hljs-built_in">void</span> {
        <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.currentMessage.trim() === <span class="hljs-string">''</span>) <span class="hljs-keyword">return</span>;

        <span class="hljs-keyword">const</span> userMsg = <span class="hljs-built_in">this</span>.currentMessage;
        <span class="hljs-built_in">this</span>.messages.push({ text: userMsg, sender: <span class="hljs-string">'user'</span> });
        <span class="hljs-built_in">this</span>.currentMessage = <span class="hljs-string">''</span>; <span class="hljs-comment">// Limpiar el input</span>

        <span class="hljs-comment">// Llama al servicio que se comunica con tu backend intermedio</span>
        <span class="hljs-built_in">this</span>.chatService.sendMessage(userMsg).subscribe({
          next: <span class="hljs-function">(<span class="hljs-params">response</span>) =&gt;</span> {
            <span class="hljs-built_in">this</span>.messages.push({ text: response.reply, sender: <span class="hljs-string">'agent'</span> });
          },
          error: <span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
            <span class="hljs-built_in">this</span>.messages.push({ text: <span class="hljs-string">'Error: No se pudo contactar al agente.'</span>, sender: <span class="hljs-string">'agent'</span> });
            <span class="hljs-built_in">console</span>.error(err);
          }
        });
    }
}
</code></pre>
<p>Por último se puede mostrar en la interfaz un diseño básico para visualizar los datos de la interacción.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"container mt-4"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"card shadow-sm"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"card-header bg-primary text-white"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">h5</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mb-0"</span>&gt;</span>Agente de Azure AI Foundry<span class="hljs-tag">&lt;/<span class="hljs-name">h5</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"card-body chat-window"</span>&gt;</span>

            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> *<span class="hljs-attr">ngFor</span>=<span class="hljs-string">"let msg of messages"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"d-flex mb-3"</span> 
                 [<span class="hljs-attr">ngClass</span>]=<span class="hljs-string">"{'justify-content-end': msg.sender === 'user'}"</span>&gt;</span>

                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"card message-bubble"</span> 
                     [<span class="hljs-attr">ngClass</span>]=<span class="hljs-string">"{'bg-primary text-white': msg.sender === 'user', 'bg-light': msg.sender === 'agent'}"</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"card-body p-2"</span>&gt;</span>
                        {{ msg.text }}
                    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"card-footer"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"input-group"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> 
                       <span class="hljs-attr">class</span>=<span class="hljs-string">"form-control"</span> 
                       [(<span class="hljs-attr">ngModel</span>)]=<span class="hljs-string">"currentMessage"</span> 
                       (<span class="hljs-attr">keyup.enter</span>)=<span class="hljs-string">"sendMessage()"</span> 
                       <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"Escribe un mensaje..."</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn btn-primary"</span> 
                        (<span class="hljs-attr">click</span>)=<span class="hljs-string">"sendMessage()"</span>
                        [<span class="hljs-attr">disabled</span>]=<span class="hljs-string">"!currentMessage.trim()"</span>&gt;</span>
                    Enviar
                <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<h2 id="heading-probar-la-integracion"><strong>Probar la integración</strong></h2>
<ul>
<li><p>Inicia tu backend Node.js.</p>
</li>
<li><p>Abre tu frontend Angular.</p>
</li>
<li><p>Envía mensajes y recibe respuestas del agente.</p>
</li>
</ul>
<h2 id="heading-recomendaciones"><strong>Recomendaciones</strong></h2>
<ul>
<li><p><strong>Seguridad</strong>: Usa HTTPS y JWT para proteger tu API.</p>
</li>
<li><p><strong>Escalabilidad</strong>: Considera usar Azure API Management para exponer tu API.</p>
</li>
<li><p><strong>Monitoreo</strong>: Activa Azure Monitor para logs y métricas.</p>
</li>
<li><p><strong>Optimización</strong>: Ajusta el prompt y la base de conocimiento para mejorar la calidad de las respuestas.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Aprendizajes al momento de probar Copilot Studio]]></title><description><![CDATA[Inserción de Variables en un contenido tipo JSON para Solicitudes HTTP


Para configurar el Body (Cuerpo) de una solicitud HTTP (Web Request) en tu Tópico, puedes usar el modo de edición JSON. La forma más limpia y recomendada para insertar los valor...]]></description><link>https://devblog.stalinmaza.com/aprendizajes-al-momento-de-probar-copilot-studio</link><guid isPermaLink="true">https://devblog.stalinmaza.com/aprendizajes-al-momento-de-probar-copilot-studio</guid><category><![CDATA[AI]]></category><category><![CDATA[copilotstudio]]></category><category><![CDATA[Agents IA]]></category><dc:creator><![CDATA[Stalin Maza]]></dc:creator><pubDate>Sun, 12 Oct 2025 02:58:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1762916479241/c44d9d22-5963-483c-a00b-41889357cac0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760237659401/d8bfdd65-0dd8-4d31-98cc-2ceec22e63c7.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-insercion-de-variables-en-un-contenido-tipo-json-para-solicitudes-http">Inserción de Variables en un contenido tipo JSON para Solicitudes HTTP</h3>
<p><img src alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760235929147/3d047054-6d64-47ba-8427-418a42071f31.png" alt class="image--center mx-auto" /></p>
<p>Para configurar el Body (Cuerpo) de una solicitud HTTP (Web Request) en tu Tópico, puedes usar el modo de edición JSON. La forma más limpia y recomendada para insertar los valores de tus variables es usando la sintaxis de expresiones de la plataforma (como Power Virtual Agents o Power Automate en un contexto de Tópico).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760235983406/94a7e3ba-43e6-4b44-ae7d-c549f0ff2a36.png" alt class="image--center mx-auto" /></p>
<p><strong>Estructura del Body (JSON con Variables)</strong></p>
<p>El método consiste en escribir la estructura JSON y reemplazar los valores dinámicos usando las variables disponibles en el contexto del tópico sea local o global.</p>
<pre><code class="lang-coffeescript">{
    filename: Topic.BannerFilename,
    scope: Global.Scope,
    storageFilePath: Topic.BannerFileStoragePath
}
</code></pre>
<p>Este enfoque de usar el contenido mediante <strong>formula</strong> garantiza que la API de destino recibirá un JSON con valores dinámicos, manteniendo el formato de datos correcto sin tener que recurrir a complejas concatenaciones de strings o funciones para concatenas una serie de valores.</p>
<h3 id="heading-manejo-de-archivos-y-la-disparidad-record-vs-file">Manejo de Archivos y la Disparidad Record vs. File</h3>
<p>Es mucho mejor y más fácil de manejar la lectura de archivos, mediante el manejo de los archivos directamente dentro del Tópico, utilizando la acción HTTP, en lugar de intentar pasar el archivo a un Flujo de Power Automate (Cloud Flow) para su procesamiento.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760236262349/2fe5dde0-e2e3-435f-a869-7d6e673d6bc4.png" alt class="image--center mx-auto" /></p>
<p>Cuando se utiliza un paso para llamar a un Flujo de Power Automate desde un Tópico con la intención de enviarle un archivo leído (que es típicamente de tipo File o Binary Content en el Tópico), ocurre un problema:</p>
<ul>
<li><p>El Tópico tiene una variable de tipo File.</p>
</li>
<li><p>El conector del Flujo de Power Automate (Cloud Flow) no acepta directamente ese tipo File. En su lugar, pide un Record (Registro/Objeto JSON) que contenga el archivo.</p>
</li>
<li><p>Resultado: El desarrollador se ve obligado a crear un objeto Record (como vimos en la sección anterior) para encapsular el contenido del archivo y pasarlo. Este paso de transformación es innecesario y propenso a errores.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760236312038/2959424c-b39e-493e-ae08-1b7753574421.png" alt class="image--center mx-auto" /></p>
<p>Para manejar este caso, la solución más eficiente es realizar la llamada a la API que procesa el archivo directamente desde el Tópico usando el paso <strong>Send an HTTP Request</strong> o similar.</p>
<p>El Tópico tiene acceso directo al contenido del archivo en Base64 y al nombre del archivo después de que el usuario lo sube. Puedes usar las variables del Tópico para construir el Body JSON (como se explica arriba) y enviarlo a la API de destino (Usando cualquier conector que permita cargar ese archivo en una nube de almacenamiento como Azure Blob Storage).</p>
<p>De esta manera, se elimina el flujo de Power Automate como intermediario, se simplifica el diseño, se evita la conversión de tipos de datos problemáticos (File a Record), y se reduce la latencia.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760236354957/a898d729-f020-47de-be82-8eee18018d56.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-manejo-de-condiciones-en-flujos-de-power-automate">Manejo de condiciones en flujos de Power Automate</h2>
<p>En Power Automate, la clave para crear ramas paralelas de manejo de errores es modificar la configuración "Ejecutar después de" de las acciones subsiguientes. Por defecto, una acción solo se ejecuta si la acción anterior fue exitosa. Al cambiar esto, puedes forzar rutas de fallo.</p>
<p>Para evitar problemas de variables en el agente (donde los outputs se convierten a minúsculas):</p>
<ul>
<li><p>Respuesta del agente: Se debe definir TODOS los parámetros de salida (ej. status, error_code, user_id).</p>
</li>
<li><p>Nomenclatura: Los nombres de los parámetros deben estar en minúsculas y usar guion bajo (_) como separador (ej., codigo_respuesta en lugar de CodigoRespuesta).</p>
</li>
</ul>
<p>Al usar esta estructura, se garantiza que:</p>
<ul>
<li>La acción de respuesta final es la misma, siempre con los mismos campos, manteniendo el contrato de la API para el agente.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760236667111/608741e8-1e9e-4d54-a5e3-d293883db71d.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-uso-de-custom-prompts-para-recoleccion-de-datos">Uso de Custom Prompts para recolección de datos</h2>
<p>Para transformar una entrada de texto libre como <code>Quiero crear el usuario John Doe, john.doe@hashnode.me, Especialista de Negocios, Panamá, +51 25454845</code>, y que la inteligencia artificial haga el trabajo de detectar ciertas variables o patrones y entregarte un objeto JSON estructurado, es esencial agregar un paso para utilizar un AI Prompt.</p>
<p><img src="https://learn.microsoft.com/en-us/ai-builder/media/use-a-custom-prompt-in-mcs/call-an-action.png" alt="Screenshot of selecting an action." /></p>
<p>Cuando tengas el prompt diseñado, debes utilizar la opción de “Probar” para ejecutar varios casos de entradas posibles y validar los resultados deseados.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760237163933/c410c306-31d7-419f-a856-2053186af194.png" alt class="image--center mx-auto" /></p>
<p>Guardar resultado del mensaje: Guarda el resultado del mensaje en una nueva variable de Tópico, por ejemplo, <code>Topic.DetectedLanguage</code>. Copilot Studio reconocerá la salida como un objeto complejo (StructuredOutput) si el mensaje especifica un JSON.</p>
<h3 id="heading-configurar-el-salto-de-preguntas-en-caso-de-que-un-valor-ya-ha-sido-capturado">Configurar el salto de preguntas en caso de que un valor ya ha sido capturado</h3>
<p>Una vez que el JSON se almacena en la variable <code>Topic.DetectedLanguage</code>, se puede aprovechar usar la funcionalidad de <strong>Omitir (Skip)</strong> en los nodos de preguntas subsiguientes para evitar preguntar por datos que ya se extrajeron.</p>
<p><strong>Añade un nodo de Pregunta:</strong> Agrega una pregunta para, por ejemplo, el <strong>Idioma</strong>.</p>
<p><strong>Configura el Comportamiento de Omitir:</strong></p>
<ol>
<li><p>Selecciona el nodo de pregunta.</p>
</li>
<li><p>En las propiedades del nodo (o en la configuración avanzada, dependiendo de la versión), busca la opción <strong>"Comportamiento de omisión"</strong> o <strong>"Skip behavior"</strong>.</p>
</li>
<li><p>Introduce la fórmula de Power Fx que verifica si el campo extraído está vacío:</p>
</li>
</ol>
<p><strong>Fórmula para el campo Idioma:</strong></p>
<p><code>If(IsBlank(Topic.DetectedLanguage) || Topic.DetectedLanguage, false, true)</code></p>
<blockquote>
<p>IsBlank(...) || ... = "": Verifica si el valor es nulo o un string vacío (""), ya que instruiste a la IA a usar un string vacío si faltaba un dato.</p>
<p>false: Si el campo está vacío (es "" o IsBlank), la condición de If es true, pero queremos ejecutar la pregunta (no omitir), por lo que retornamos false para la omisión.</p>
<p>true: Si el campo contiene un valor, la condición es false, por lo que retornamos true para la omisión (saltarse el paso).</p>
</blockquote>
<h3 id="heading-conservar-el-formato-de-los-prompts">Conservar el Formato de los Prompts</h3>
<p>Cuando estás creando un Mensaje de IA (AI Prompt) en Copilot Studio, especialmente aquellos complejos que incluyen instrucciones detalladas, estructuras JSON o ejemplos de few-shot, es crucial que el formato (espacios, saltos de línea y tabulaciones) se mantenga intacto. El problema ocurre al copiar y pegar directamente desde editores de código avanzados como VS Code, Sublime Text o incluso aplicaciones de procesamiento de texto con formato (Word, Google Docs):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760237587882/811e9519-2bc2-4af0-a55f-ab55818ccf3c.jpeg" alt class="image--center mx-auto" /></p>
<p><strong>El Problema del Formato Enriquecido</strong><br />Los editores avanzados a menudo incluyen caracteres invisibles o un formato de texto enriquecido (como tabulaciones especiales, saltos de línea de bloque, o metadatos de formato) que son diferentes de los simples caracteres de texto plano que necesita Copilot Studio.</p>
<p><strong>Cuando pegas directamente</strong></p>
<p>Copilot Studio (o el campo de texto subyacente) puede intentar interpretar estos caracteres especiales. El resultado es un prompt deformado o un texto que el modelo de IA ya no puede leer correctamente, lo que lleva a resultados inesperados o errores en la extracción/generación. Por ejemplo, los saltos de línea necesarios para separar las instrucciones del JSON pueden desaparecer.<br />Para evitar este problema, la mejor práctica es utilizar una herramienta de edición de texto plano como el Bloc de Notas (Notepad) en Windows, TextEdit (en modo texto plano) en Mac, o cualquier editor simple:</p>
<ul>
<li><p>Copia tu prompt desde VS Code u otro editor avanzado.</p>
</li>
<li><p>Pégalo en el Bloc de Notas. Al hacerlo, el Bloc de Notas elimina automáticamente todo el formato enriquecido y los caracteres invisibles, dejando solo el texto puro.</p>
</li>
<li><p>Copia el contenido del Bloc de Notas.</p>
</li>
<li><p>Pégalo en el campo de Mensaje de IA de Copilot Studio.</p>
</li>
</ul>
<p>Este paso intermedio garantiza que solo se transfieren los saltos de línea y los espacios estándar, preservando la estructura lógica de tu prompt para que el modelo de IA lo interprete de manera predecible y correcta.</p>
]]></content:encoded></item><item><title><![CDATA[Migrando a Spring Boot 3: Un Viaje a la Modernización (y sus Desafíos)]]></title><description><![CDATA[Si eres un desarrollador de Spring Boot, sabes que mantenerte actualizado es clave. La llegada de Spring Boot 3 trae consigo un cambio importante y necesario: la transición de java.util.Date a la API de tiempo de Java (java.time.*). Aunque este cambi...]]></description><link>https://devblog.stalinmaza.com/migrando-a-spring-boot-3-un-viaje-a-la-modernizacion-y-sus-desafios</link><guid isPermaLink="true">https://devblog.stalinmaza.com/migrando-a-spring-boot-3-un-viaje-a-la-modernizacion-y-sus-desafios</guid><category><![CDATA[Springboot]]></category><category><![CDATA[Java]]></category><category><![CDATA[gradle]]></category><dc:creator><![CDATA[Stalin Maza]]></dc:creator><pubDate>Fri, 19 Sep 2025 03:42:19 GMT</pubDate><content:encoded><![CDATA[<p>Si eres un desarrollador de Spring Boot, sabes que mantenerte actualizado es clave. La llegada de Spring Boot 3 trae consigo un cambio importante y necesario: la transición de <a target="_blank" href="http://java.util.Date"><code>java.util.Date</code></a> a la API de tiempo de Java (<code>java.time.*</code>). Aunque este cambio es fundamental para la modernización y la seguridad de nuestras aplicaciones, no es un simple reemplazo de un tipo de dato por otro.</p>
<p>Recientemente me enfrenté a este proceso y quiero compartir los puntos clave que debes considerar para que tu migración sea lo más fluida posible.</p>
<h4 id="heading-1-adios-a-javautildatehttpjavautildate-hola-a-localdate">1. Adiós a <a target="_blank" href="http://java.util.Date"><code>java.util.Date</code></a>, Hola a <code>LocalDate</code></h4>
<p>El primer y más evidente cambio es dejar atrás <code>Date</code>. Si bien <a target="_blank" href="http://java.util.Date"><code>java.util.Date</code></a> funcionaba, era una clase con problemas conocidos de mutabilidad y manejo de zonas horarias. La nueva API <code>java.time</code> ofrece tipos específicos para cada necesidad:</p>
<ul>
<li><p><code>LocalDate</code>: Para fechas (<code>2023-11-03</code>).</p>
</li>
<li><p><code>LocalTime</code>: Para horas (<code>15:30:00</code>).</p>
</li>
<li><p><code>LocalDateTime</code>: Para fecha y hora (<code>2023-11-03T15:30:00</code>).</p>
</li>
</ul>
<p>El reto aquí no es solo cambiar el tipo en tu código, sino también la forma en que los datos se manejan en el backend y el frontend.</p>
<h4 id="heading-2-la-serializacion-del-timestamp-al-array">2. La Serialización: Del Timestamp al Array</h4>
<p>Aquí es donde surge la principal fricción con las aplicaciones existentes. Por defecto, cuando Jackson (la biblioteca de serialización de Spring) encuentra un campo <a target="_blank" href="http://java.util.Date"><code>java.util.Date</code></a>, lo convierte a un <strong>timestamp</strong> (un número largo). Sin embargo, con <code>LocalDate</code>, el comportamiento por defecto es serializarlo como un <strong>array JSON</strong> (<code>[2023, 11, 3]</code>).</p>
<p>Si tu frontend espera un timestamp, esto causará errores. La solución es configurar la serialización de Jackson en el backend para que <code>LocalDate</code> se convierta a un timestamp. Esto se puede lograr fácilmente con la anotación <code>@JsonFormat</code> directamente en el campo de tu entidad:</p>
<pre><code class="lang-java"><span class="hljs-keyword">import</span> com.fasterxml.jackson.annotation.JsonFormat;
<span class="hljs-comment">// ...</span>
<span class="hljs-meta">@JsonFormat(shape = JsonFormat.Shape.NUMBER_INT)</span>
<span class="hljs-keyword">private</span> LocalDate fechaDePublicacion;
</code></pre>
<h4 id="heading-3-actualizaciones-en-la-configuracion-de-jpa-y-hibernate">3. Actualizaciones en la Configuración de JPA y Hibernate</h4>
<p>La validación de esquemas en Spring Boot 3 es mucho más estricta. Ya no puedes confiar en que Hibernate "adivine" el tipo de dato correcto. Si tienes columnas con tipos específicos, ahora debes definirlas explícitamente:</p>
<ul>
<li><p><strong>Decimales</strong>: Olvídate de <code>@Column(columnDefinition = "decimal", precision = 10, scale = 2)</code>. Ahora debes usar el formato estándar de SQL: <code>@Column(columnDefinition = "DECIMAL(10, 2)")</code>.</p>
</li>
<li><p><strong>Tipos de Columna</strong>: Si usabas <code>@Type</code> para especificar el tipo de una columna, ahora la mejor práctica es usar la anotación <code>@Column</code> directamente.</p>
</li>
</ul>
<h4 id="heading-4-cambios-en-el-manejo-de-fechas-en-el-codigo">4. Cambios en el Manejo de Fechas en el Código</h4>
<p>Al migrar tu lógica de negocio, tus métodos que antes usaban <code>Date</code> ahora deben ser reescritos para usar <code>LocalDate</code>. Por ejemplo, para sumar días, el método <code>plusDays()</code> de <code>LocalDate</code> es mucho más claro y sencillo que la manipulación de milisegundos que se hacía con <code>Date</code>.</p>
<p><strong>Antes:</strong></p>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Date <span class="hljs-title">generateFixedDate</span><span class="hljs-params">(Date dateWithOffset, <span class="hljs-keyword">int</span> offset)</span> </span>{
    <span class="hljs-keyword">long</span> epochMilli = dateWithOffset.toInstant().toEpochMilli() + ...;
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Date(epochMilli);
}
</code></pre>
<p><strong>Ahora</strong></p>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> LocalDate <span class="hljs-title">generateFixedDate</span><span class="hljs-params">(LocalDate dateWithOffset, <span class="hljs-keyword">int</span> offset)</span> </span>{
    <span class="hljs-keyword">return</span> dateWithOffset.plusDays(offset);
}
</code></pre>
<p>Cuando migras una aplicación de Spring Boot 2 a 3, uno de los desafíos más grandes es lidiar con los cambios en las dependencias y la configuración. Aquí está el ítem número 5 para tu blog, enfocado en cómo manejar el cambio de <code>javax</code> a <code>jakarta</code>.</p>
<hr />
<h2 id="heading-5-el-gran-cambio-de-javax-a-jakarta-tu-nueva-lista-de-tareas">5. El gran cambio de <code>javax</code> a <code>jakarta</code>: Tu nueva lista de tareas</h2>
<p>Si hay una única tarea que debes priorizar al migrar a Spring Boot 3, es la de <strong>actualizar tus paquetes de</strong> <code>javax</code> a <code>jakarta</code>. Este cambio no es una simple cuestión de estilo; es una modificación fundamental en la API de Java EE (ahora conocida como Jakarta EE). El equipo de Spring Boot adoptó por completo el nuevo ecosistema, y tus proyectos deben hacer lo mismo. Ignorar este paso resultará en errores de compilación y de tiempo de ejecución.</p>
<h3 id="heading-que-paquetes-son-los-mas-afectados">¿Qué paquetes son los más afectados?</h3>
<ul>
<li><p><strong>JPA (Java Persistence API):</strong> Si tu aplicación usa Hibernate, la mayoría de tus entidades, como <code>@Entity</code>, <code>@Table</code>, y <code>@Id</code>, deben cambiar su importación de <code>javax.persistence</code> a <code>jakarta.persistence</code>. Sin esta actualización, tu <code>EntityManagerFactory</code> no se inicializará y el proyecto no se ejecutará.</p>
</li>
<li><p><strong>Servlets:</strong> Las clases relacionadas con el manejo de peticiones web, como <code>HttpServletRequest</code> y <code>HttpServletResponse</code>, deben cambiar de <code>javax.servlet</code> a <code>jakarta.servlet</code>.</p>
</li>
<li><p><strong>Validación:</strong> Las anotaciones de validación como <code>@Valid</code> o <code>@Size</code> también migran de <code>javax.validation</code> a <code>jakarta.validation</code>.</p>
</li>
<li><p><strong>JAXB y JTA:</strong> Los paquetes para la serialización de XML y la gestión de transacciones distribuidas (<code>javax.xml.bind</code> y <code>javax.transaction</code>) también se mueven a <code>jakarta.xml.bind</code> y <code>jakarta.transaction</code>, respectivamente.</p>
</li>
</ul>
<h3 id="heading-como-hacerlo-de-forma-eficiente">¿Cómo hacerlo de forma eficiente?</h3>
<p>En lugar de ir archivo por archivo, utiliza las herramientas de tu IDE. La mayoría de los entornos de desarrollo como <strong>IntelliJ IDEA</strong> tienen una función de búsqueda y reemplazo a nivel de proyecto. Simplemente busca <code>javax.</code> y reemplázalo por <code>jakarta.</code>. Ten cuidado de no reemplazar los paquetes que no migraron, como <code>javax.crypto</code> que es parte del JDK estándar.</p>
<p>Al hacer este cambio, estarás alineado con la última versión de la plataforma Java, lo que te permitirá aprovechar las nuevas características y mejoras de rendimiento de Spring Boot 3 y sus dependencias.</p>
<h3 id="heading-conclusion">Conclusión</h3>
<p>La migración a Spring Boot 3 es un paso inevitable hacia adelante. Aunque la transición de <code>Date</code> a <code>LocalDate</code> presenta desafíos en la serialización y validación, la inversión de tiempo vale la pena. Ganas en claridad del código, seguridad de tipos y te alineas con los estándares modernos de Java.</p>
<p>Es un proceso que requiere paciencia y atención a los detalles, pero al final, tu aplicación será más robusta y fácil de mantener.</p>
]]></content:encoded></item><item><title><![CDATA[Cómo resolver problemas de cookies seguras en proyectos SaaS con múltiples dominios]]></title><description><![CDATA[En el desarrollo de aplicaciones SaaS multi-tenant, es común enfrentarse a problemas relacionados con el manejo de cookies seguras cuando los portales de los clientes usan dominios distintos. Esto se debe a que los navegadores modernos aplican políti...]]></description><link>https://devblog.stalinmaza.com/como-resolver-problemas-de-cookies-seguras-en-proyectos-saas-con-multiples-dominios</link><guid isPermaLink="true">https://devblog.stalinmaza.com/como-resolver-problemas-de-cookies-seguras-en-proyectos-saas-con-multiples-dominios</guid><category><![CDATA[manejo de sesiones]]></category><category><![CDATA[Sass]]></category><category><![CDATA[cookies]]></category><dc:creator><![CDATA[Stalin Maza]]></dc:creator><pubDate>Fri, 19 Sep 2025 03:28:38 GMT</pubDate><content:encoded><![CDATA[<p>En el desarrollo de aplicaciones SaaS multi-tenant, es común enfrentarse a problemas relacionados con el manejo de cookies seguras cuando los portales de los clientes usan dominios distintos. Esto se debe a que los navegadores modernos aplican políticas muy estrictas de seguridad (SameSite, Secure, HttpOnly, bloqueo de third-party cookies). En este blog quiero compartir un caso real que nos ocurrió y cómo lo solucionamos.</p>
<p><strong>El inconveniente inicial</strong></p>
<p>El proyecto SaaS que manejaba estaba implementado en <strong>Kubernetes en Azure</strong>, usando cookies seguras para el manejo de sesión. Mientras todos los portales vivían bajo el mismo dominio raíz, no tuvimos inconvenientes.</p>
<p>Ejemplo:</p>
<ul>
<li><p>Frontend: <a target="_blank" href="http://portal.miempresa.com"><code>portal_bi.miempresa.com</code></a></p>
</li>
<li><p>Backend: <a target="_blank" href="http://api.miempresa.com"><code>api_bi.miempresa.com</code></a></p>
</li>
</ul>
<p>Aquí las cookies funcionaban bien porque compartían el mismo dominio (<code>.</code><a target="_blank" href="http://miempresa.com"><code>miempresa.com</code></a>).</p>
<p>Sin embargo, cuando un nuevo cliente necesitó su propio dominio:</p>
<ul>
<li><p>Frontend: <a target="_blank" href="http://portalcliente.com"><code>portalcliente_bi.dominioexterno.com</code></a></p>
</li>
<li><p>Backend: <a target="_blank" href="http://api.miempresa.com"><code>api_bi.miempresa.dominioexterno.com</code></a></p>
</li>
</ul>
<p>El inicio de sesión comenzó a fallar en ciertos casos:</p>
<ul>
<li><p>En <strong>modo incógnito</strong> el inicio de sesión no funcionaba.</p>
</li>
<li><p>A algunos usuarios corporativos, con navegadores más estrictos, también les fallaba.</p>
</li>
</ul>
<p>La causa: el navegador detectaba que las cookies venían de un dominio distinto y las bloqueaba por política de seguridad.</p>
<blockquote>
<p>Por ejemplo:</p>
<ul>
<li><p>En Brave, el portal funcionaba tanto en el modo incognito y en las ventanas normales, a pesar de que en la consola de desarrollo no se listaban las cookies seguras.</p>
</li>
<li><p>En Edge, Chrome y Firefox, en la ventana normal si aparecían las cookies seguras mientras que en el modo incognito no aparecían y se cerraba la sesión.</p>
</li>
</ul>
</blockquote>
<h2 id="heading-primera-hipotesis">Primera hipótesis</h2>
<p>Pensamos que tal vez el error estaba ligado únicamente a configuraciones de cookies, así que hicimos pruebas replicando el escenario en un <strong>subdominio de pruebas</strong>:</p>
<ul>
<li><a target="_blank" href="http://clientepruebas.dominio-test.com"><code>clientepruebas.dominio-test.com</code></a></li>
</ul>
<p>Al probar login en ventana normal e incógnito, vimos que en incógnito también se bloqueaban las cookies si el backend estaba en un dominio distinto. Confirmamos que el problema <strong>no era de configuración de cookies</strong>, sino de <strong>cross-domain</strong>.</p>
<h2 id="heading-solucion-planteada">Solución planteada</h2>
<p>La solución que adoptamos fue <strong>dejar de usar una URL estática de backend</strong> y, en su lugar, hacer que el frontend siempre invoque a la API relativa al mismo dominio donde está desplegado el cliente.</p>
<p>Antes:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Llamada "quemada"</span>
fetch(<span class="hljs-string">"https://miportal.com/api/v2/endpoint"</span>)
</code></pre>
<p>Después:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Llamada relativa al dominio actual</span>
fetch(<span class="hljs-string">"/api/v2/endpoint"</span>)
</code></pre>
<p>De esta manera, si el cliente está en:</p>
<ul>
<li><p><a target="_blank" href="http://portalcliente.com"><code>portalcliente.com</code></a>, la llamada va a <a target="_blank" href="http://portalcliente.com/api/v2"><code>portalcliente.com/api/v2</code></a>.</p>
</li>
<li><p><a target="_blank" href="http://clientepruebas.dominio-test.com"><code>clientepruebas.dominio-test.com</code></a>, la llamada va a <a target="_blank" href="http://clientepruebas.dominio-test.com/api/v2"><code>clientepruebas.dominio-test.com/api/v2</code></a>.</p>
</li>
</ul>
<p>Y en Kubernetes configuramos el <strong>Ingress</strong> para que todas las llamadas a <code>/api/v2</code> se redirijan internamente al servicio backend.</p>
<h2 id="heading-ventajas-de-este-enfoque">Ventajas de este enfoque</h2>
<ol>
<li><p><strong>Misma política de dominio</strong>: al hacer las llamadas desde el mismo dominio donde está montado el frontend, las cookies seguras se comportan como first-party, no third-party.</p>
</li>
<li><p><strong>Evita bloqueos de navegadores</strong>: ya no dependemos de excepciones de Chrome, Edge, Safari o configuraciones corporativas.</p>
</li>
<li><p><strong>Escalable para múltiples clientes</strong>: cada dominio de cliente puede usar <code>/api/v2</code> sin necesidad de cambiar la configuración del backend.</p>
</li>
<li><p><strong>Más limpio en Kubernetes</strong>: el Ingress enruta todas las peticiones <code>/api/v2</code> al servicio backend sin importar desde qué dominio se accede.</p>
</li>
</ol>
<hr />
<h2 id="heading-conclusion">Conclusión</h2>
<p>Si estás desarrollando un SaaS multi-tenant y manejas sesiones con cookies seguras, debes considerar que <strong>los navegadores no permiten compartir cookies entre dominios distintos</strong>. Esto puede romper el inicio de sesión en incógnito o en entornos corporativos.</p>
<p>Para resolverlo, una estrategia eficaz es:</p>
<ul>
<li><p>Evitar URLs absolutas en el frontend.</p>
</li>
<li><p>Usar rutas relativas (<code>/api/...</code>).</p>
</li>
<li><p>Configurar el Ingress de Kubernetes para enrutar esas rutas al backend.</p>
</li>
</ul>
<p>De esta manera, cada cliente accede a tu SaaS con su propio dominio y las cookies seguras funcionan sin restricciones.</p>
<hr />
<p>¿Te pasó algo parecido en tu proyecto SaaS? 🚀 Déjame tus comentarios y qué soluciones aplicaste para manejar cookies y sesiones en entornos multi-dominio.</p>
]]></content:encoded></item><item><title><![CDATA[Conectar JDK Mission Control con aplicación Java en Docker (Local)]]></title><description><![CDATA[Si deseas poder conectarte a tu aplicación de Java para poder revisar el rendimiento de la misma utilizando JMX y tu aplicación esta dockerizada, puedes utilizar los siguientes pasos para poder realizarlo.
Hay que añadir algunas variables de configur...]]></description><link>https://devblog.stalinmaza.com/conectar-jdk-mission-control-con-aplicacion-java-en-docker-local</link><guid isPermaLink="true">https://devblog.stalinmaza.com/conectar-jdk-mission-control-con-aplicacion-java-en-docker-local</guid><category><![CDATA[Docker]]></category><category><![CDATA[Java]]></category><category><![CDATA[jdk mission control]]></category><dc:creator><![CDATA[Stalin Maza]]></dc:creator><pubDate>Tue, 29 Aug 2023 17:52:23 GMT</pubDate><content:encoded><![CDATA[<p>Si deseas poder conectarte a tu aplicación de Java para poder revisar el rendimiento de la misma utilizando JMX y tu aplicación esta dockerizada, puedes utilizar los siguientes pasos para poder realizarlo.</p>
<p>Hay que añadir algunas variables de configuración de JMX para que cuando se ejecute la aplicación, permita el acceso sin necesidad de aplicar SSH, sin solicitar autenticación con la siguiente configuración en docker.</p>
<h2 id="heading-dockerfile"><strong>Dockerfile</strong></h2>
<h2 id="heading-ia"> </h2>
<p>```dockerfile</p>
<p>CMD ["java", "-<a target="_blank" href="http://Dcom.sun.management">Dcom.sun.management</a>.jmxremote.port=9090", "-<a target="_blank" href="http://Dcom.sun.management">Dcom.sun.management</a>.jmxremote.rmi.port=9090", "-<a target="_blank" href="http://Dcom.sun.management">Dcom.sun.management</a>.jmxremote.authenticate=false", "-<a target="_blank" href="http://Dcom.sun.management">Dcom.sun.management</a>.jmxremote.ssl=false", "-Djava.rmi.server.hostname=<a target="_blank" href="http://localhost">localhost</a>", "-jar", "/opt/handytec-portal/handytec-portal-backend-1.0.0.jar"]<br />```<br />- <strong>Puerto 9090</strong> lo usará JMC para conectarse a la app en docker.<br />- <strong>hostname</strong> localhost para que utilize la red local una vez docker expone el puerto local de nuestra PC<br />- <strong>ssl=false</strong> para desactivar que necesitemos ssl al conectarnos<br />- <strong>authenticate=false</strong> para no necesitar credenciales al loguearnos</p>
<h2 id="heading-ejecutar-contenedor"><strong>Ejecutar contenedor</strong></h2>
<p>Para ejecutar el contenedor debemos ejecutar el siguiente comando:</p>
<p>```bash<br />docker run -it --rm --env-file=./.env -p 8080:8080 -p 9090:9090 --name &lt;containerName&gt; &lt;imageName&gt;</p>
<p>```</p>
<p>- <strong>-it</strong>: Habilita el modo interactivo al ejecutar la imagen</p>
<p>- <strong>--rm</strong>: Eliminar el contenedor al terminar de utilizarlo</p>
<p>- <strong>--env-file=./.env</strong>: Cargar las variables de entorno al contenedor en base a un archivo <strong>.env</strong></p>
<p>- <strong>-p 8080:8080</strong>: Mapear puertos desde el host hacia el contenedor &lt;host:contenedor&gt;</p>
<p>- <strong>--name &lt;containerName&gt;</strong>: Nombre del contenedor</p>
<p>- <strong>&lt;imageName&gt;</strong>: Nombre de la imagen de la cual se va a crear un contenedor</p>
<p>En este ejemplo se habilita el puerto 8080 para que lo utiliza la aplicación de Java y el puerto 9090 para que acceda el JMX.</p>
<h2 id="heading-jdk-mission-control">JDK Mission Control</h2>
<p>Al añadir una nueva conexión en la aplicación de JDK Mission Control, se solicita los siguientes parámetros:<br />- <strong>host</strong>: localhost<br />- <strong>port</strong>: 9090</p>
<p>Con esto, se puede hacer clic en el botón "Probar conexión" y en el apartado de "Status" debe salir "Success". Opcionalmente, se puede añadir un nombre personalizado a la conexión.</p>
]]></content:encoded></item><item><title><![CDATA[Implementar cierre de sesión debido a inactividad del usuario]]></title><description><![CDATA[Si alguna vez en tu aplicación necesitas implementar la lógica de cierre de sesión automatico cuando tus usuarios han dejado de ejecutar alguna actividad en la aplicación, te comparto un ejemplo de implementación, en este caso, usando Angular y Fireb...]]></description><link>https://devblog.stalinmaza.com/implementar-cierre-de-sesion-debido-a-inactividad-del-usuario</link><guid isPermaLink="true">https://devblog.stalinmaza.com/implementar-cierre-de-sesion-debido-a-inactividad-del-usuario</guid><category><![CDATA[Angular]]></category><category><![CDATA[Firebase]]></category><category><![CDATA[RxJS]]></category><dc:creator><![CDATA[Stalin Maza]]></dc:creator><pubDate>Sun, 07 May 2023 17:09:11 GMT</pubDate><content:encoded><![CDATA[<p>Si alguna vez en tu aplicación necesitas implementar la lógica de cierre de sesión automatico cuando tus usuarios han dejado de ejecutar alguna actividad en la aplicación, te comparto un ejemplo de implementación, en este caso, usando Angular y Firebase.</p>
<p>Para llevarlo a cabo te comparto los aspectos a cubrir:</p>
<ul>
<li><p>El cierre de sesión se ejecuta luego de 10 minutos de inactividad (se puede cambiar en base a una variable).</p>
</li>
<li><p>Para detectar la actividad del usuario, en este caso porque la aplicación lo requiere, se escucha los eventos del mouse [mousemove,click], si ninguno de estos sucede dentro de 10 minutos, se da por hecho que el usuario ha dejado de interactuar con la aplicación.</p>
</li>
</ul>
<p>Para esto se implementa la función "resetInactiveUserOnApp" la cual es declarada dentro del archivo "authentication.service.ts", el cual contiene las funciones para realizar la autenticación vía firebase como el login, logout, refreshToken, etc.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//authentication.service.ts</span>
<span class="hljs-keyword">const</span> minutesToMilliseconds = <span class="hljs-function">(<span class="hljs-params">minutes: number</span>) =&gt;</span> minutes * <span class="hljs-number">60000</span>;
<span class="hljs-keyword">const</span> AUTO_LOGOUT_INTERVAL_WAIT = minutesToMilliseconds(<span class="hljs-number">10</span>);

private pauseInactiveListener = <span class="hljs-keyword">new</span> BehaviourSubject&lt;boolean&gt;(<span class="hljs-literal">false</span>);
private resetTimerSubscription = <span class="hljs-keyword">new</span> Subscription();

<span class="hljs-keyword">constructor</span>(){
    <span class="hljs-comment">// Inicializar función al crear el componente</span>
    <span class="hljs-built_in">this</span>.initializeInactiveListener();
}

login(){
    <span class="hljs-comment">//Ejecutar lógica de login y al final llamar a la función para</span>
    <span class="hljs-comment">// inicializar el listener de inactive user. Esto debido a que</span>
    <span class="hljs-comment">// al ejecutar el logout se limpia la suscripción y se </span>
    <span class="hljs-comment">// debe reinicializar la función</span>
    <span class="hljs-built_in">this</span>.initializeInactiveListener();
}

logout(){
    <span class="hljs-comment">// Ejecutar lógica de cierre de sesión y al final </span>
    <span class="hljs-comment">// limpiar suscripciones</span>
    <span class="hljs-built_in">this</span>.cleanComponentSubscriptions();
}

<span class="hljs-comment">// Abre un modal con un contador de 60 segundos que permite al usuario</span>
<span class="hljs-comment">// elegir si desea o no continuar con la sesión abierta, si termina el</span>
<span class="hljs-comment">// contador y no se ha elegido alguna opción, se cierra la sesión</span>
<span class="hljs-comment">// automaticamente</span>
openLogoutModal(){
    <span class="hljs-comment">// Pausar la escucha de eventos cuando se abre el modal para validar</span>
    <span class="hljs-comment">// si el usuario desea continuar con la sesión abierta</span>
    <span class="hljs-built_in">this</span>.pauseInactiveListener.next(<span class="hljs-literal">true</span>);
    <span class="hljs-keyword">const</span> dialogRef = <span class="hljs-built_in">this</span>.dialog.open(LogoutCountdownComponent, {
      <span class="hljs-attr">width</span>: <span class="hljs-string">'350px'</span>,
      <span class="hljs-attr">data</span>: {},
      <span class="hljs-attr">panelClass</span>: <span class="hljs-string">'dialog-draggable'</span>,
      <span class="hljs-attr">hasBackdrop</span>: <span class="hljs-literal">true</span>,
      <span class="hljs-attr">disableClose</span>: <span class="hljs-literal">true</span>
    });
    <span class="hljs-comment">// Cuando se cierre la sesión, el modal devolvera un booleano </span>
    <span class="hljs-comment">// que sera true o false dependiendo de si se desea o no</span>
    <span class="hljs-comment">// continuar con la sesión abierta</span>
    dialogRef.afterClosed().subscribe(<span class="hljs-function"><span class="hljs-params">keepSession</span> =&gt;</span> {
      <span class="hljs-keyword">if</span> (keepSession) {
        <span class="hljs-comment">// Refrescar la sesión y luego habilita la escucha de eventos</span>
        <span class="hljs-built_in">this</span>.refreshSession().pipe(take(<span class="hljs-number">1</span>)).subscribe(<span class="hljs-function">() =&gt;</span> {
          <span class="hljs-built_in">this</span>.pauseInactiveListener.next(<span class="hljs-literal">false</span>);
        })
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-comment">// Ejecuta el logout</span>
        <span class="hljs-built_in">this</span>.logout().then(<span class="hljs-function">() =&gt;</span> {
          <span class="hljs-built_in">this</span>.router.navigate([<span class="hljs-string">'/login'</span>]);
        });
      }
    });
}

initializeInactiveListener(){
  <span class="hljs-comment">//Escuchar los eventos del mouse que tomamos en cuenta</span>
  <span class="hljs-comment">// para determinar que el usuario esta interactuando con la página</span>
  <span class="hljs-keyword">const</span> mouseMove$ = fromEvent(<span class="hljs-built_in">document</span>, <span class="hljs-string">'mousemove'</span>);
  <span class="hljs-keyword">const</span> click$ = fromEvent(<span class="hljs-built_in">document</span>, <span class="hljs-string">'click'</span>);
  <span class="hljs-comment">// Pausar la escucha de los eventos cuando el observable de pausa esta</span>
  <span class="hljs-comment">// habilitado, esto se ejecuta al abrir el modal de log out.</span>
  <span class="hljs-built_in">this</span>.resetTimerSubscription = merge(mouseMove$, click$)
    .pipe(
      filter(<span class="hljs-function">() =&gt;</span> !<span class="hljs-built_in">this</span>.pauseInactiveListener.value),
      <span class="hljs-comment">// Usando el switchmap se cancela el observable anterior si</span>
      <span class="hljs-comment">// todavía no se ejecuta, en este caso luego de 10min y </span>
      <span class="hljs-comment">// se completa el observable si una nueva emisión es ejecutada</span>
      switchMap(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">return</span> timer(AUTO_LOGOUT_INTERVAL_WAIT).pipe(
          takeUntil(merge(mouseMove$, click$))
        );
      })
    ).subscribe(<span class="hljs-function">() =&gt;</span> {
      <span class="hljs-comment">// Al obtener una emisión que cumple las validaciones antes</span>
      <span class="hljs-comment">// añadidas, se verifica la validez de la sesión y si no es</span>
      <span class="hljs-comment">// válida, se valida el observable de pausa, si esta desactivado</span>
      <span class="hljs-comment">// abre la ventana de logout de modal</span>
      <span class="hljs-built_in">this</span>.verifySessionIsValid().toPromise().then(<span class="hljs-function"><span class="hljs-params">credentials</span> =&gt;</span> {
        <span class="hljs-keyword">if</span> (credentials) {
          <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">this</span>.pauseInactiveListener.value) {
            <span class="hljs-built_in">this</span>.openLogoutModal();
          }
        }
      });
    });
}

cleanComponentSubscriptions(){
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.resetTimerSubscription) {
        <span class="hljs-built_in">this</span>.resetTimerSubscription.unsubscribe();
    }
}
<span class="hljs-comment">// Limpiar la suscripción del timer cuando el componente se destruye</span>
ngOnDestroy(){
   <span class="hljs-built_in">this</span>.cleanComponentSubscriptions();
}
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Exportando datos a PDF con Angular]]></title><description><![CDATA[En esta publicación vamos a crear un servicio que permita exportar los datos de una tabla hacia un documento PDF, los cuales van a mostrarse en columnas autoajustadas.
📦 Paquetes necesarios
Primero lo que debemos hacer es importar los paquetes neces...]]></description><link>https://devblog.stalinmaza.com/exportando-datos-a-pdf-con-angular</link><guid isPermaLink="true">https://devblog.stalinmaza.com/exportando-datos-a-pdf-con-angular</guid><category><![CDATA[Angular]]></category><category><![CDATA[pdf]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Stalin Maza]]></dc:creator><pubDate>Tue, 15 Dec 2020 01:51:39 GMT</pubDate><content:encoded><![CDATA[<p>En esta publicación vamos a crear un servicio que permita exportar los datos de una tabla hacia un documento PDF, los cuales van a mostrarse en columnas autoajustadas.</p>
<p><strong>📦 Paquetes necesarios</strong></p>
<p>Primero lo que debemos hacer es importar los paquetes necesarios, entre los principales que se necesitarían son:</p>
<ul>
<li><p><strong>FileSaver</strong>: para poder guardar archivos desde el navegador</p>
</li>
<li><p><strong>JSPDF</strong>: librería para generar PDF´s con JavaScript</p>
</li>
<li><p><strong>JSPDF Autotable</strong>: complemento de jspdf para habilitar la capacidad de generar tablas PDF ya sea analizando tablas HTML o utilizando datos JavaScript directamente</p>
</li>
</ul>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Injectable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> FileSaver <span class="hljs-keyword">from</span> <span class="hljs-string">'file-saver'</span>;
<span class="hljs-keyword">import</span> jsPDF <span class="hljs-keyword">from</span> <span class="hljs-string">'jspdf'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'jspdf-autotable'</span>;
<span class="hljs-keyword">import</span> { reduceArrayByKeys } <span class="hljs-keyword">from</span> <span class="hljs-string">'src/app/shared/helpers/utils'</span>;
<span class="hljs-keyword">import</span> { MessagesService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./messages.service'</span>;
</code></pre>
<p>📝 <strong>Interfaz para cabeceras</strong></p>
<p>Lo que necesitamos luego es crear una interfaz para mantener un tipado de los datos de entrada que tendremos al momento de exportar los datos.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">interface</span> IExportHeaders {
    header: <span class="hljs-built_in">string</span>;
    dataKey: <span class="hljs-built_in">string</span>;
}
</code></pre>
<p>🔍 <strong>Función para generar cabeceras por defecto</strong></p>
<p>En caso de que no se provean las cabeceras de los datos, esta función permite obtenerlas a partir de las keys de los objetos.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> getDefaultHeaders = <span class="hljs-function">(<span class="hljs-params">json</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> defaultHeaders = [];
    <span class="hljs-keyword">const</span> keysObj = <span class="hljs-built_in">Object</span>.keys(json);
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> value <span class="hljs-keyword">of</span> keysObj) {
        <span class="hljs-keyword">const</span> tempObj = {
            header: value, dataKey: value
        };
        defaultHeaders.push(tempObj)
    }
    <span class="hljs-keyword">return</span> defaultHeaders;
}
</code></pre>
<p><strong>⚙️ Configuración de PDF</strong></p>
<p>Luego de eso se procede a escribir la configuración del objeto de JSPDF donde se puede configurar entre otras cosas la orientación del papel.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> PDFConfig = { <span class="hljs-attr">putOnlyUsedFonts</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">orientation</span>: <span class="hljs-string">'landscape'</span> };
</code></pre>
<p>Hecho eso ya podemos estructurar el servicio con su decorador tal cual como cualquier servicio normal de Angular, en el cual importamos un servicio para mostrar mensajes en el constructor del servicio.</p>
<pre><code class="lang-typescript"><span class="hljs-meta">@Injectable</span>({ providedIn: <span class="hljs-string">'root'</span> })
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> ArchivosService {

    <span class="hljs-keyword">constructor</span>(<span class="hljs-params">
        <span class="hljs-keyword">private</span> messagesService: MessagesService
    </span>) { }
</code></pre>
<p>Luego si especificamos la función que hará el trabajo de crear el PDF y guardarlo en el dispositivo del usuario. En este caso recibimos 4 parámetros:</p>
<ul>
<li><p>data: lo especificamos con un array de cualquier tipo ya que va a ser utilizado por múltiples componentes en nuestra aplicación</p>
</li>
<li><p>headers: se recibe las cabeceras que se mostrarán en las columnas de la tabla</p>
</li>
<li><p>filename: el nombre del archivo a exportarse</p>
</li>
<li><p>headerTitle: el título que aparecerá</p>
</li>
</ul>
<pre><code class="lang-typescript">exportPDF(
    data: <span class="hljs-built_in">any</span>[],
    headers: IExportHeaders[] = [],
    filename = <span class="hljs-string">'file'</span>,
    headerTitle = <span class="hljs-string">'Documento eFact'</span>
)
</code></pre>
<p>Luego verificamos si recibimos un array vacío, en ese caso retornamos un mensaje indicando que no existen datos disponibles para exportar.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">if</span> (data.length === <span class="hljs-number">0</span>) {
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.messagesService.info_notification(
        <span class="hljs-string">'No hay datos disponibles para exportar'</span>
    );
}
</code></pre>
<p>También verificamos que si no se enviaron las cabeceras, las obtenemos usando la función que definimos anteriormente</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">if</span> (!headers || headers.length === <span class="hljs-number">0</span>) {
    headers = getDefaultHeaders(data[<span class="hljs-number">0</span>]);
}
</code></pre>
<p>Una vez que pasaron los datos las validaciones anteriores, se configura el documento jsPDF en el cual se configura el título del documento con tamaño de letra <strong>18</strong> y lo demás del documento con tamaño <strong>8</strong>.</p>
<pre><code class="lang-typescript"><span class="hljs-built_in">this</span>.messagesService.info_notification(<span class="hljs-string">'Generando su documento PDF...'</span>);
<span class="hljs-keyword">const</span> doc = <span class="hljs-keyword">new</span> jsPDF(PDFConfig);
doc.setFontSize(<span class="hljs-number">18</span>);
doc.text(headerTitle, <span class="hljs-number">14</span>, <span class="hljs-number">14</span>);
doc.setFontSize(<span class="hljs-number">8</span>);

<span class="hljs-keyword">const</span> objColumns = {};
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> header <span class="hljs-keyword">of</span> headers) {
  objColumns[header.dataKey] = { columnWidth: <span class="hljs-string">'auto'</span> };
}
</code></pre>
<p>Luego de eso configuramos el método <strong>autoTable</strong> que se añade en el objeto jsPDF donde se establecen algunos parámetros como las columnas, el contenido de la tabla, la configuración de ancho de las columnas, el tema de la tabla entre otras configuraciones más.</p>
<pre><code class="lang-js">doc.autoTable({
  <span class="hljs-attr">columns</span>: headers,
  <span class="hljs-attr">body</span>: data,
  <span class="hljs-attr">startY</span>: <span class="hljs-number">20</span>,
  <span class="hljs-attr">columnStyles</span>: objColumns,
  <span class="hljs-attr">theme</span>: <span class="hljs-string">'striped'</span>,
  <span class="hljs-attr">tableWidth</span>: <span class="hljs-string">'auto'</span>,
  <span class="hljs-attr">cellWidth</span>: <span class="hljs-string">'wrap'</span>,
  <span class="hljs-attr">showHead</span>: <span class="hljs-string">'firstPage'</span>,
  <span class="hljs-attr">headStyles</span>: { <span class="hljs-attr">fillColor</span>: [<span class="hljs-number">52</span>, <span class="hljs-number">152</span>, <span class="hljs-number">219</span>] },
  <span class="hljs-attr">styles</span>: {
    <span class="hljs-attr">overflow</span>: <span class="hljs-string">'linebreak'</span>,
    <span class="hljs-attr">cellWidth</span>: <span class="hljs-string">'wrap'</span>,
    <span class="hljs-attr">fontSize</span>: <span class="hljs-number">8</span>,
    <span class="hljs-attr">cellPadding</span>: <span class="hljs-number">2</span>,
    <span class="hljs-attr">overflowColumns</span>: <span class="hljs-string">'linebreak'</span>
  }
});
</code></pre>
<p>Para obtener el numero de paginas, se usa el método <strong>getNumberOfPages</strong> que provee el método internal del documento y se procede a calcular el alto de la página.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> pageNumber = doc.internal.getNumberOfPages();
<span class="hljs-keyword">const</span> pageSize = doc.internal.pageSize;
<span class="hljs-keyword">const</span> pageHeight = pageSize.height || pageSize.getHeight();
</code></pre>
<p>Por último se ejecuta la función <strong>addFooters</strong> que se encarga de añadir el número de página en cada página del documento, el texto puede ser personalizado así como la ubicación del mismo. En el documento se establece el número de páginas y se ejecuta el método <strong>save</strong> del documento JSPDF.</p>
<pre><code class="lang-typescript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addFooters</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; pageNumber; i++) {
    doc.text(<span class="hljs-string">`Página <span class="hljs-subst">${i + <span class="hljs-number">1</span>}</span> de <span class="hljs-subst">${pageNumber}</span>`</span>, <span class="hljs-number">14</span>, pageHeight - <span class="hljs-number">10</span>);
  }
}

addFooters();
doc.setPage(pageNumber);
doc.save(<span class="hljs-string">`<span class="hljs-subst">${filename}</span>`</span>);
</code></pre>
<p>Un ejemplo de como usar este servicio seria de esta manera, importándolo en el constructor de tu componente y llamando al método del servicio.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">constructor</span>(<span class="hljs-params">
    <span class="hljs-keyword">private</span> _archivosService: ArchivosService 
</span>){
}


print(){
    <span class="hljs-keyword">const</span> headers = [
        {  header: <span class="hljs-string">'Id'</span>, dataKey: <span class="hljs-string">'id'</span> },
        {  header: <span class="hljs-string">'Nombre'</span>, dataKey: <span class="hljs-string">'nombre'</span>}
    ];
    <span class="hljs-keyword">const</span> data = [
        { id: <span class="hljs-number">1</span>, nombre: <span class="hljs-string">'Alexander Arnold'</span> },
        { id: <span class="hljs-number">2</span>, nombre: <span class="hljs-string">'Mariela Lopez'</span> }
    ];
    <span class="hljs-built_in">this</span>.exportPDF(data, headers, <span class="hljs-string">"usuarios.pdf"</span>, <span class="hljs-string">"Usuarios"</span>)
}
</code></pre>
<p>Listo, ya has escrito tu primer servicio que te permitirá exportar documentos PDF mandando un array de datos, el cual claramente lo puedes modificar para imprimir lo que consideres necesario.</p>
<p><strong>Ejemplo:</strong></p>
<h2 id="heading-estructura-del-ejemplo">📂 Estructura del ejemplo</h2>
<ul>
<li><p><strong>archivos.service.ts</strong> → Tu servicio para exportar a PDF (ya lo tienes).</p>
</li>
<li><p><strong>usuarios.component.ts</strong> → Componente que usa el servicio.</p>
</li>
<li><p><strong>usuarios.component.html</strong> → Vista con tabla y botón.</p>
</li>
<li><p><strong>usuarios.component.css</strong> → Estilos básicos.</p>
</li>
</ul>
<p><strong>usuarios.component.ts</strong></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Component } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { ArchivosService } <span class="hljs-keyword">from</span> <span class="hljs-string">'../services/archivos.service'</span>;

<span class="hljs-meta">@Component</span>({
  selector: <span class="hljs-string">'app-usuarios'</span>,
  templateUrl: <span class="hljs-string">'./usuarios.component.html'</span>,
  styleUrls: [<span class="hljs-string">'./usuarios.component.css'</span>]
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> UsuariosComponent {

  <span class="hljs-comment">// Datos de ejemplo</span>
  usuarios = [
    { id: <span class="hljs-number">1</span>, nombre: <span class="hljs-string">'Alexander Arnold'</span>, email: <span class="hljs-string">'alex@example.com'</span> },
    { id: <span class="hljs-number">2</span>, nombre: <span class="hljs-string">'Mariela Lopez'</span>, email: <span class="hljs-string">'mariela@example.com'</span> },
    { id: <span class="hljs-number">3</span>, nombre: <span class="hljs-string">'Carlos Pérez'</span>, email: <span class="hljs-string">'carlos@example.com'</span> }
  ];

  <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> archivosService: ArchivosService</span>) {}

  exportarPDF() {
    <span class="hljs-keyword">const</span> headers = [
      { header: <span class="hljs-string">'ID'</span>, dataKey: <span class="hljs-string">'id'</span> },
      { header: <span class="hljs-string">'Nombre'</span>, dataKey: <span class="hljs-string">'nombre'</span> },
      { header: <span class="hljs-string">'Email'</span>, dataKey: <span class="hljs-string">'email'</span> }
    ];

    <span class="hljs-built_in">this</span>.archivosService.exportPDF(
      <span class="hljs-built_in">this</span>.usuarios,
      headers,
      <span class="hljs-string">'usuarios.pdf'</span>,
      <span class="hljs-string">'Lista de Usuarios'</span>
    );
  }
}
</code></pre>
<p><strong>usuarios.component.html</strong></p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"container"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>Lista de Usuarios<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">table</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">thead</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">tr</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">th</span>&gt;</span>ID<span class="hljs-tag">&lt;/<span class="hljs-name">th</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">th</span>&gt;</span>Nombre<span class="hljs-tag">&lt;/<span class="hljs-name">th</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">th</span>&gt;</span>Email<span class="hljs-tag">&lt;/<span class="hljs-name">th</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">tr</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">thead</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">tbody</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">tr</span> *<span class="hljs-attr">ngFor</span>=<span class="hljs-string">"let usuario of usuarios"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>{{ usuario.id }}<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>{{ usuario.nombre }}<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">td</span>&gt;</span>{{ usuario.email }}<span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">tr</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">tbody</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">table</span>&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">button</span> (<span class="hljs-attr">click</span>)=<span class="hljs-string">"exportarPDF()"</span>&gt;</span>📄 Exportar a PDF<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p><strong>usuarios.component.css</strong></p>
<pre><code class="lang-css"><span class="hljs-selector-class">.container</span> {
  <span class="hljs-attribute">max-width</span>: <span class="hljs-number">600px</span>;
  <span class="hljs-attribute">margin</span>: auto;
  <span class="hljs-attribute">text-align</span>: center;
  <span class="hljs-attribute">font-family</span>: Arial, sans-serif;
}

<span class="hljs-selector-tag">h2</span> {
  <span class="hljs-attribute">margin-bottom</span>: <span class="hljs-number">20px</span>;
}

<span class="hljs-selector-tag">table</span> {
  <span class="hljs-attribute">width</span>: <span class="hljs-number">100%</span>;
  <span class="hljs-attribute">border-collapse</span>: collapse;
  <span class="hljs-attribute">margin-bottom</span>: <span class="hljs-number">15px</span>;
}

<span class="hljs-selector-tag">table</span>, <span class="hljs-selector-tag">th</span>, <span class="hljs-selector-tag">td</span> {
  <span class="hljs-attribute">border</span>: <span class="hljs-number">1px</span> solid <span class="hljs-number">#ccc</span>;
}

<span class="hljs-selector-tag">th</span> {
  <span class="hljs-attribute">background-color</span>: <span class="hljs-number">#3498db</span>;
  <span class="hljs-attribute">color</span>: white;
  <span class="hljs-attribute">padding</span>: <span class="hljs-number">8px</span>;
}

<span class="hljs-selector-tag">td</span> {
  <span class="hljs-attribute">padding</span>: <span class="hljs-number">8px</span>;
}

<span class="hljs-selector-tag">button</span> {
  <span class="hljs-attribute">background-color</span>: <span class="hljs-number">#2ecc71</span>;
  <span class="hljs-attribute">color</span>: white;
  <span class="hljs-attribute">border</span>: none;
  <span class="hljs-attribute">padding</span>: <span class="hljs-number">10px</span> <span class="hljs-number">15px</span>;
  <span class="hljs-attribute">cursor</span>: pointer;
  <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">4px</span>;
}

<span class="hljs-selector-tag">button</span><span class="hljs-selector-pseudo">:hover</span> {
  <span class="hljs-attribute">background-color</span>: <span class="hljs-number">#27ae60</span>;
}
</code></pre>
<h2 id="heading-como-funciona">🔹 Cómo funciona</h2>
<ol>
<li><p>El usuario ve la tabla con los datos.</p>
</li>
<li><p>Al hacer clic en <strong>"Exportar a PDF"</strong>, se llama al método <code>exportarPDF()</code> del componente.</p>
</li>
<li><p>Este método prepara las cabeceras y llama al servicio <code>ArchivosService</code>.</p>
</li>
<li><p>El servicio genera el PDF con <code>jsPDF</code> y <code>autoTable</code> y lo descarga automáticamente.</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Google o Facebook no me retorna el correo de registro]]></title><description><![CDATA[Muchas veces cuando añadimos el inicio de sesión por medio de Redes Sociales como Facebook o Google y estamos realizando las pruebas nos podemos topar con el error de que el API no nos devuelva el campo email.
En el caso de Facebook en la documentaci...]]></description><link>https://devblog.stalinmaza.com/google-o-facebook-no-me-retorna-el-correo-de-registro</link><guid isPermaLink="true">https://devblog.stalinmaza.com/google-o-facebook-no-me-retorna-el-correo-de-registro</guid><dc:creator><![CDATA[Stalin Maza]]></dc:creator><pubDate>Fri, 29 May 2020 02:38:04 GMT</pubDate><content:encoded><![CDATA[<p>Muchas veces cuando añadimos el inicio de sesión por medio de Redes Sociales como Facebook o Google y estamos realizando las pruebas nos podemos topar con el error de que el API no nos devuelva el campo email.</p>
<p>En el caso de Facebook en la documentación podemos encontrar lo siguiente: </p>
<blockquote>
<p>Campo Email: Este campo no se devolverá si no hay una dirección de correo electrónico válida disponible para el usuario</p>
</blockquote>
<p>Leyendo esto podemos deducir que cuando una dirección de correo electrónico no ha sido verificado o no existe, como en el caso de alguien que se registro por medio de su numero telefónico.</p>
<h1 id="fuentes-referenciadas-">Fuentes Referenciadas:</h1>
<ul>
<li><a target='_blank' rel='noopener noreferrer'  href="https://developers.facebook.com/docs/reference/api/user/" title="Facebook Api]">Facebook Api</a> </li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Consumiendo mi Primera Api con Leaflet y Javascript]]></title><description><![CDATA[Esta publicación la escribo gracias a algunos pedidos de personas que les intereso el ejercicio que hice sobre un mapa que muestre los datos de contagiados por coronavirus en el Mundo usando el API de : Wuhan Coronavirus
Para este ejemplo lo primero ...]]></description><link>https://devblog.stalinmaza.com/consumiendo-mi-primera-api-con-leaflet-y-javascript</link><guid isPermaLink="true">https://devblog.stalinmaza.com/consumiendo-mi-primera-api-con-leaflet-y-javascript</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[APIs]]></category><category><![CDATA[coronavirus]]></category><dc:creator><![CDATA[Stalin Maza]]></dc:creator><pubDate>Sat, 21 Mar 2020 01:49:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1584756668925/_25Ist5wr.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Esta publicación la escribo gracias a algunos pedidos de personas que les intereso el ejercicio que hice sobre un mapa que muestre los datos de contagiados por coronavirus en el Mundo usando el API de : <a target="_blank" href="https://wuhan-coronavirus-api.laeyoung.endpoint.ainize.ai/jhu-edu/latest">Wuhan Coronavirus</a></p>
<p>Para este ejemplo lo primero que debemos tener es nuestro esquema HTML, en el cual cargamos los archivos CSS y Javascript de la Librería Leaflet para usar los mapas en alternativa a Google Maps, además de algunos plugins que robustecen la funcionalidad de Leaflet y por supuesto debemos incluir un elemento HTML, en este caso un <strong>DIV</strong> donde se mostrará el Mapa.</p>
<pre><code class="lang-xml"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"es"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- Etiquetas para que en dispositivos móviles se pueda ver correctamente el modo en pantalla completa--&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"UTF-8"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>COVID 19<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- Cargar los Estilos del Mapa--&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"./assets/leaflet-core/leaflet.css"</span> /&gt;</span>
    <span class="hljs-comment">&lt;!-- Cargar el archivo JS del Mapa--&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"./assets/leaflet-core/leaflet-src.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span> 
<span class="hljs-comment">&lt;!-- Cargar el archivo CSS y JS del Plugin para Pantalla Completa en el Mapa--&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"./assets/leaflet-fullscreen/Control.FullScreen.css"</span> /&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"./assets/leaflet-fullscreen/Control.FullScreen.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- Hoja de Estilos Propia --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"style.css"</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- Plugin para mejorar la carga de las capas de los Mapas --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"./assets/leaflet.edgebuffer.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- Plugin para poder cambiar el estilo de color de la Capa a Usar --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"./assets//leaflet-tilelayer-colorfilter.min.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- Contenedor donde se va a mostrar el Mapa --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"map"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"map"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- Archivos Javascript Propios --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"module"</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"main.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>Una vez generado la estructura HTML podemos seguir con el archivo JS para darle funcionalidad a nuestro ejercicio.</p>
<p>Primeramente declaramos las variables a utilizar:</p>
<pre><code class="lang-js"><span class="hljs-comment">//Para la capa que utilizara el Mapa</span>
<span class="hljs-keyword">let</span> lightLayer = <span class="hljs-string">'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'</span>;
<span class="hljs-comment">// Constantes para guardar los iconos para el Modo Oscuro / Claro</span>
<span class="hljs-keyword">const</span> darkIcon = <span class="hljs-string">'🌛'</span>;
<span class="hljs-keyword">const</span> lightIcon = <span class="hljs-string">'☀️'</span>;
<span class="hljs-comment">// Constantes para guardar los valores por defecto para el modo oscuro y modo claro</span>
<span class="hljs-keyword">const</span> filterDark = [<span class="hljs-string">'invert:100%'</span>];
<span class="hljs-keyword">const</span> filterLight = [];
<span class="hljs-comment">// Variable donde guardaremos el filtro Actual</span>
<span class="hljs-keyword">let</span> currentFilter = [];
<span class="hljs-comment">// Variable donde guardaremos el icono del boton actual</span>
<span class="hljs-keyword">let</span> btnIcon = lightIcon;
<span class="hljs-comment">// Constante donde guardaremos la atribución a mostrar en el Mapa</span>
<span class="hljs-keyword">const</span> leafletAtribution = <span class="hljs-string">'&amp;copy; &lt;a href="https://www.openstreetmap.org/copyright"&gt;Gracias a OpenStreetMap&lt;/a&gt;'</span>;
</code></pre>
<p>También necesitamos algunas funciones que nos permitan manejar el LocalStorage ya que guardaremos el tema que escoja el usuario para que la próxima vez que recargue la página, se quede guardado el tema que se haya escogido a la hora de visualizar el mapa</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Función para obtener el tema guardado, en caso que no haya alguno devolverá el tema claro por defecto</span>
<span class="hljs-keyword">const</span> getThemeMode = <span class="hljs-function">() =&gt;</span>{
    <span class="hljs-keyword">const</span> mode = <span class="hljs-built_in">localStorage</span>.getItem(<span class="hljs-string">'sm-mode-theme'</span>);
    <span class="hljs-keyword">if</span>(!mode){
        <span class="hljs-keyword">return</span> <span class="hljs-string">'light'</span>
    }<span class="hljs-keyword">else</span>{
        <span class="hljs-keyword">return</span> mode;
    }
}
<span class="hljs-comment">// Función para guardar el tema escogido en el LocalStorage</span>
<span class="hljs-keyword">const</span> setThemeMode = <span class="hljs-function">(<span class="hljs-params">mode</span>) =&gt;</span> {
    <span class="hljs-built_in">localStorage</span>.setItem(<span class="hljs-string">'sm-mode-theme'</span>, mode);
}
<span class="hljs-comment">// Función para alternar el Tema cuando se haga click en el boton de alternar tema</span>
<span class="hljs-keyword">const</span> toggleThemeMode = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> mode = getThemeMode();
    <span class="hljs-keyword">if</span>(mode == <span class="hljs-string">'light'</span>){
        setThemeMode(<span class="hljs-string">'dark'</span>);
    }<span class="hljs-keyword">else</span>{
        setThemeMode(<span class="hljs-string">'light'</span>);
    }
}
</code></pre>
<p>Hecho esto realizamos algunas validaciones al cargar la página para ofrecer la mejor experiencia al usuario detectando si las preferencias del usuario en el modo de visualización</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Verificamos el esquema de color de preferencia que tenga configurado el usuario, si es el tema oscuro lo guardamos caso contrario se toma por entendido que la preferencia es el tema claro</span>
<span class="hljs-keyword">if</span> (<span class="hljs-built_in">window</span>.matchMedia &amp;&amp; <span class="hljs-built_in">window</span>.matchMedia(<span class="hljs-string">'(prefers-color-scheme: dark)'</span>).matchess) {
    <span class="hljs-built_in">localStorage</span>.setItem(<span class="hljs-string">'sm-mode-theme'</span>, <span class="hljs-string">'dark'</span>);
}
<span class="hljs-comment">// Hecho esto verificamos que en el LocalStorage exista un valor de tema por defecto, si no existe guardamos el modo Claro por Defecto</span>
<span class="hljs-keyword">if</span>(<span class="hljs-built_in">localStorage</span>.getItem(<span class="hljs-string">"sm-mode-theme"</span>) == <span class="hljs-literal">null</span>){
    setThemeMode(<span class="hljs-string">'light'</span>);
}
<span class="hljs-comment">// Actualizamos el filtro que debemos usar en el Mapa dependiendo del tema guardado en el Local Storage</span>
currentFilter = (getThemeMode() == <span class="hljs-string">'light'</span>) ? filterLight: filterDark;
</code></pre>
<p>Lo siguiente que debemos hacer es crear el Mapa usando la variable <strong>L</strong> que exporta la librería Leaflet](https://leafletjs.com/reference-1.6.0.html#map-option) y nos permita inicializar un Mapa pasándole como parámetros, primeramente el ID del elemento HTML donde se va a mostrar y después un objeto con los parámetros que podemos sobrescribir, las que usaremos en este tutorial son:</p>
<ul>
<li><p>zoomAnimation: activar/desactivar la animación realizada al hacer Zoom</p>
</li>
<li><p>markerZoomAnimation: activar/desactivar la animación del marcador</p>
</li>
<li><p>zoomControl: mostrar o ocultar el botón de control de Zoom</p>
</li>
</ul>
<p>Además configuramos las coordenadas por defecto que cargara el mapa, y el nivel de Zoom que tendrá en la opción <strong>setView</strong></p>
<pre><code class="lang-js"><span class="hljs-keyword">let</span> map = L.map(<span class="hljs-string">'map'</span>, {
    <span class="hljs-attr">zoomAnimation</span>: <span class="hljs-literal">false</span>,
    <span class="hljs-attr">markerZoomAnimation</span>: <span class="hljs-literal">false</span>,
    <span class="hljs-attr">zoomControl</span>: <span class="hljs-literal">true</span>,
}).setView([<span class="hljs-number">0</span>, <span class="hljs-number">0</span>], <span class="hljs-number">3</span>);
</code></pre>
<p>Luego creamos una capa usando el filtro de la librería <strong>Color Filter</strong> pasándole como primer parámetro la URL de la Capa y como segundo parámetro las opciones de configuración de la Capa.</p>
<ul>
<li><p>attribution: Mensaje a Mostrar en el mapa dando los créditos a los autores</p>
</li>
<li><p>filter: filtro de Color a utilizar</p>
</li>
</ul>
<pre><code class="lang-js"><span class="hljs-keyword">let</span> mapTileLayer = L.tileLayer.colorFilter(lightLayer, {
    <span class="hljs-attr">attribution</span>: leafletAtribution,
    <span class="hljs-attr">updateWhenIdle</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">reuseTiles</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">filter</span>: currentFilter,
}).addTo(map);
</code></pre>
<p>Además añadimos en el mapa un control para el modo de pantalla completa.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">let</span> fsControl = L.control.fullscreen();
map.addControl(fsControl);
</code></pre>
<p>Hecho esto vamos a crear un control personalizado para agregarlo al mapa. Lo primero que debemos hacer es inicializar un objeto tipo <strong>Control</strong> y en la propiedad <strong>onAdd</strong> pasarle una función que retorna un elemento de la clase <strong>DomUtil</strong>, en este caso de tipo <strong>input</strong>, al cual le asignamos el tipo como <strong>botón</strong> y el valor será el icono del botón. Por último retornamos el objeto creado y añadimos ese control al mapa.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">let</span> btnThemeControl = L.control();
btnThemeControl.onAdd = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">map</span>) </span>{
    <span class="hljs-keyword">let</span> container = L.DomUtil.create(<span class="hljs-string">'input'</span>);
    container.type = <span class="hljs-string">"button"</span>;
    container.value = btnIcon;
    container.title = <span class="hljs-string">"Modo Visualización"</span>;
    <span class="hljs-comment">//Función que alterna el tema, actualiza la capa y actualiza la clase y el value del botón.</span>
    container.onclick = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">event</span>) </span>{
        toggleThemeMode();
        <span class="hljs-comment">//Cambiar Boton, Clase y Filtro de Mapa</span>
        <span class="hljs-keyword">if</span>(getThemeMode() == <span class="hljs-string">'light'</span>){
            event.target.value = lightIcon;
            event.target.classList.remove(<span class="hljs-string">'dark'</span>);
            mapTileLayer.updateFilter(filterLight);
        }<span class="hljs-keyword">else</span>{
            event.target.value = darkIcon;
            mapTileLayer.updateFilter(filterDark);
            event.target.classList.add(<span class="hljs-string">'dark'</span>);
        }
    }
    container.classList.add(<span class="hljs-string">'btnThemeControl'</span>)
    <span class="hljs-keyword">return</span> container;
};
btnThemeControl.addTo(map);
</code></pre>
<p>También necesitamos una función que haga la petición al API y nos devuelva la respuesta con los datos necesarios para mostrarlos en el Mapa</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//Funcion para traer los datos de un API y retornar el JSON de Respuesta</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getData</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'https://wuhan-coronavirus-api.laeyoung.endpoint.ainize.ai/jhu-edu/latest'</span>)
    <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> response.json()
    <span class="hljs-keyword">return</span> data
}
</code></pre>
<p>La siguiente función que usaremos será para generar el HTML que se mostrara en el pop-up del Marcador una vez le demos clic, en este pop-up mostraremos el País, el numero de personas contagiadas, fallecidas y recuperadas</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//Función renderizar datos</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">renderExtraData</span>(<span class="hljs-params">{ confirmed, deaths, recovered, provincestate, countryregion }</span>) </span>{
    <span class="hljs-keyword">return</span> (<span class="hljs-string">`
        &lt;div&gt;
          &lt;p&gt; &lt;strong&gt;<span class="hljs-subst">${provincestate}</span> - <span class="hljs-subst">${countryregion}</span>&lt;/strong&gt; &lt;/p&gt;
          &lt;p&gt; Confirmados: <span class="hljs-subst">${confirmed}</span> &lt;/p&gt;
          &lt;p&gt; Muertes: <span class="hljs-subst">${deaths}</span> &lt;/p&gt;
          &lt;p&gt; Recuperados: <span class="hljs-subst">${recovered}</span> &lt;/p&gt;
        &lt;/div&gt;
      `</span>)
}
</code></pre>
<p>Lo siguiente que debemos crear es el Icono personalizado a mostrar en los marcadores que creemos, los recursos los pueden encontrar en el repositorio de Github que lo dejo al final del Post. Leaflet tiene un objeto <strong>Icon</strong> que cuando lo inicializamos le podemos mandar parámetros como la URL del Icono, del Shadow o Sombra del Marcador, el tamaño de la sombra y del icono, entre otros.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> iconUrl = <span class="hljs-string">'./icon.png'</span>;
<span class="hljs-keyword">const</span> shadowIcon = <span class="hljs-string">'./marker-shadow.png'</span>;
<span class="hljs-keyword">const</span> icon = <span class="hljs-keyword">new</span> L.Icon({
    iconUrl: iconUrl,
    shadowUrl: shadowIcon,
    shadowSize: [<span class="hljs-number">40</span>, <span class="hljs-number">40</span>],
    iconSize: [<span class="hljs-number">40</span>, <span class="hljs-number">40</span>],
    iconAnchor: [<span class="hljs-number">12</span>, <span class="hljs-number">41</span>],
    popupAnchor: [<span class="hljs-number">1</span>, <span class="hljs-number">-34</span>],
});
</code></pre>
<p>Finalmente codificamos la función que renderizara los datos en la pantalla, y lo que hace es lo siguiente:</p>
<ul>
<li><p>Obtener los datos del Api</p>
</li>
<li><p>Recorrer la lista de información y por cada dato crear un marcador, asignarles las coordenadas y el icono del marcador, para añadirlos al mapa, sin olvidarnos de setear el contenido del Popup.</p>
</li>
</ul>
<pre><code class="lang-typescript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">renderData</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> getData();
    data.forEach(<span class="hljs-function">(<span class="hljs-params">item, index</span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> marker = L.marker([item.location.lat, item.location.lng], { icon: icon })
        .bindPopup(renderExtraData(item))
        .addTo(map);
    });
}
</code></pre>
<p>Lo que debemos hacer al final es llamar a la función para renderizar los datos.</p>
<pre><code class="lang-js">renderData()
</code></pre>
<p><strong>Postdata</strong> Adjunto los estilos utilizados</p>
<pre><code class="lang-css"><span class="hljs-selector-tag">body</span> {
  <span class="hljs-attribute">margin</span>: <span class="hljs-number">0</span>;
}
<span class="hljs-selector-class">.map</span> {
  <span class="hljs-attribute">width</span>: <span class="hljs-number">100vw</span>;
  <span class="hljs-attribute">height</span>: <span class="hljs-number">100vh</span>;
}

<span class="hljs-selector-class">.info</span> {
  <span class="hljs-attribute">padding</span>: <span class="hljs-number">6px</span> <span class="hljs-number">8px</span>;
  <span class="hljs-attribute">font</span>: <span class="hljs-number">14px</span>/<span class="hljs-number">16px</span> Arial, Helvetica, sans-serif;
  <span class="hljs-attribute">background</span>: <span class="hljs-number">#22303a</span>;
  <span class="hljs-attribute">box-shadow</span>: <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">15px</span> <span class="hljs-built_in">rgba</span>(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0.2</span>);
  <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">5px</span>;
  <span class="hljs-attribute">padding</span>: <span class="hljs-number">12px</span> <span class="hljs-number">10px</span>;
  <span class="hljs-attribute">text-align</span>: center;
}
<span class="hljs-selector-class">.info</span> <span class="hljs-selector-tag">h4</span> {
  <span class="hljs-attribute">margin</span>: <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">5px</span>;
  <span class="hljs-attribute">font-weight</span>: bold;
  <span class="hljs-attribute">color</span>: white;
}
<span class="hljs-selector-class">.info</span> <span class="hljs-selector-tag">span</span> {
  <span class="hljs-attribute">color</span>: <span class="hljs-number">#66d1ff</span>;
}
<span class="hljs-selector-class">.btnThemeControl</span> {
  <span class="hljs-attribute">padding</span>: <span class="hljs-number">8px</span>;
  <span class="hljs-attribute">font-size</span>: <span class="hljs-number">24px</span>;
  <span class="hljs-attribute">background</span>: white;
  <span class="hljs-attribute">outline</span>: none <span class="hljs-meta">!important</span>;
  <span class="hljs-attribute">border</span>: none <span class="hljs-meta">!important</span>;
  <span class="hljs-attribute">cursor</span>: pointer;
  <span class="hljs-attribute">height</span>: <span class="hljs-number">50px</span>;
  <span class="hljs-attribute">width</span>: <span class="hljs-number">50px</span>;
  <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">50%</span>;
}
<span class="hljs-selector-class">.btnThemeControl</span><span class="hljs-selector-pseudo">:focus</span> {
  <span class="hljs-attribute">outline</span>: none <span class="hljs-meta">!important</span>;
}
<span class="hljs-selector-class">.btnThemeControl</span><span class="hljs-selector-pseudo">::-moz-focus-inner</span> {
  <span class="hljs-attribute">border</span>: <span class="hljs-number">0</span> <span class="hljs-meta">!important</span>;
}
<span class="hljs-selector-class">.btnThemeControl</span><span class="hljs-selector-class">.dark</span> {
  <span class="hljs-attribute">background</span>: <span class="hljs-number">#383434</span>;
}
<span class="hljs-selector-class">.leaflet-center</span> {
  <span class="hljs-attribute">left</span>: <span class="hljs-number">50%</span>;
  <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">translate</span>(-<span class="hljs-number">50%</span>, <span class="hljs-number">0%</span>);
  <span class="hljs-attribute">margin</span>: <span class="hljs-number">1em</span>;
}
</code></pre>
<p>Muchas gracias por leer esta publicación, si te gusto por favor compártela con quien lo necesite, puedes revisar el <a target="_blank" href="https://github.com/StalinMazaEpn/CoronaVirusMap2019">repositorio</a> y observar el resultado final en este <a target="_blank" href="https://corona-sm-2019-app.netlify.com/">enlace</a></p>
]]></content:encoded></item><item><title><![CDATA[Django, la guía que me ha ayudado a manejarlo eficazmente]]></title><description><![CDATA[En mi trabajo me toco manejar el backend con Django y se me hizo bastante duro el comprenderlo, además que no me gustaba mucho python debido a que por un mal espaciado produce errores, prefiero usar corchetes y punto y coma.
Pero el trabajo me solici...]]></description><link>https://devblog.stalinmaza.com/django-la-guia-que-me-ha-ayudado-a-manejarlo-eficazmente</link><guid isPermaLink="true">https://devblog.stalinmaza.com/django-la-guia-que-me-ha-ayudado-a-manejarlo-eficazmente</guid><category><![CDATA[Python]]></category><category><![CDATA[Django]]></category><dc:creator><![CDATA[Stalin Maza]]></dc:creator><pubDate>Sat, 22 Feb 2020 03:07:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1583103158649/xxOX8iAzP.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>En mi trabajo me toco manejar el backend con Django y se me hizo bastante duro el comprenderlo, además que no me gustaba mucho python debido a que por un mal espaciado produce errores, prefiero usar corchetes y punto y coma.</p>
<p>Pero el trabajo me solicitaba utilizarlo y así pude descubrir su potencial que habiendo manejado lenguajes backend como JS con NodeJS, PHP con Laravel y ahora Python con Django, mi orden de frameworks más recomendados y optimizados serián:</p>
<ul>
<li>NodeJS</li>
<li>Django</li>
<li>Laravel</li>
</ul>
<p>Algunos tips y funciones útiles que puedes o no utilizarlas van a ser listadas y el blog se ira actualizando mediante la profundización en el framework siga avanzando.</p>
<h3 id="lanzar-errores-propios-en-formato-json">Lanzar Errores Propios en formato JSON</h3>
<pre><code class="lang-python"><span class="hljs-comment"># Importar la librería</span>
<span class="hljs-keyword">from</span> rest_framework.exceptions <span class="hljs-keyword">import</span> APIException
<span class="hljs-comment"># Utilizar la librería</span>
<span class="hljs-keyword">raise</span> APIException(<span class="hljs-string">"el mensaje a ser retornado"</span>)
</code></pre>
<h3 id="manejo-de-migraciones">Manejo de Migraciones</h3>
<p>Cuando se necesite resetear las migraciones debido a algún conflicto que tengamos o porque no se aplican los cambios hechos, debemos tomar en cuenta dos casos:</p>
<h4 id="-si-podemos-borrar-toda-la-bdd-"><em>Si podemos borrar toda la BDD</em></h4>
<p>Lo primero que debemos hacer es borrar todos los archivos en la carpeta migraciones excepto el archivo <strong><strong>init</strong>.py</strong> .</p>
<pre><code>find . -path <span class="hljs-string">"*/migrations/*.py"</span> -<span class="hljs-keyword">not</span> -name <span class="hljs-string">"__init__.py"</span> -<span class="hljs-keyword">delete</span>
find . -path <span class="hljs-string">"*/migrations/*.pyc"</span>  -<span class="hljs-keyword">delete</span>
</code></pre><p>Luego de eso podemos crear de nuevo las migraciones con el comando:</p>
<pre><code class="lang-cmd">python manage.py makemigrations
</code></pre>
<p>y ejecutamos las migraciones con el comando: </p>
<pre><code class="lang-bash">python manage.py migrate
</code></pre>
<h4 id="-si-borrar-toda-la-bdd-no-es-posible-"><em>Si borrar toda la BDD no es posible</em></h4>
<p>Primero debemos limpiar las migraciones con el comando:</p>
<pre><code><span class="hljs-selector-tag">python</span> <span class="hljs-selector-tag">manage</span><span class="hljs-selector-class">.py</span> <span class="hljs-selector-tag">migrate</span> <span class="hljs-selector-tag">--fake</span> <span class="hljs-selector-tag">nombreAplicacion</span> <span class="hljs-selector-tag">zero</span>
</code></pre><p>Si queremos comprobar que se limpiaron correctamente ejecutamos el siguiente comando y nos listara de cada aplicación las migraciones respectivas</p>
<pre><code><span class="hljs-selector-tag">python</span> <span class="hljs-selector-tag">manage</span><span class="hljs-selector-class">.py</span> <span class="hljs-selector-tag">showmigrations</span>
</code></pre><p>Luego de eso procedemos a borrar todos los archivos en la carpeta migraciones excepto el archivo <strong><strong>init</strong>.py</strong> .</p>
<pre><code>find . -path <span class="hljs-string">"*/migrations/*.py"</span> -<span class="hljs-keyword">not</span> -name <span class="hljs-string">"__init__.py"</span> -<span class="hljs-keyword">delete</span>
find . -path <span class="hljs-string">"*/migrations/*.pyc"</span>  -<span class="hljs-keyword">delete</span>
</code></pre><p>Luego de eso podemos crear de nuevo las migraciones con el comando:</p>
<pre><code><span class="hljs-selector-tag">python</span> <span class="hljs-selector-tag">manage</span><span class="hljs-selector-class">.py</span> <span class="hljs-selector-tag">makemigrations</span>
</code></pre><p>y ejecutamos las migraciones agregando la bandera <strong>--fake-initial</strong> para que django marque las migraciones como ejecutadas cuando realmente no lo hace, para evitar el problema de que algunas tablas ya existen: </p>
<pre><code><span class="hljs-selector-tag">python</span> <span class="hljs-selector-tag">manage</span><span class="hljs-selector-class">.py</span> <span class="hljs-selector-tag">migrate</span> <span class="hljs-selector-tag">--fake-initial</span>
</code></pre><h2 id="exportar-importar-datos-con-django">Exportar/Importar datos con Django</h2>
<p>Para crear un fixture con los datos actuales el comando es:</p>
<pre><code><span class="hljs-attribute">python</span> manage.py dumpdata api &gt; api/fixtures/all_2020_02_25_final.json
</code></pre><p>Para cargar datos desde el fixture creado</p>
<pre><code><span class="hljs-attribute">python</span> manage.py loaddata api/fixtures/all_2020_02_25_final.json
</code></pre><p>Hecho esto podemos trabajar normalmente con este gran Framework de Python</p>
<h3 id="autor">Autor</h3>
<blockquote>
<p>Stalin Maza</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Angular, solución a problemas comunes]]></title><description><![CDATA[Esconder el header del mat-stepper de Angular Material
En caso de que necesitemos ocultar la cabecera donde se muestran los pasos del stepper que nos ofrece angular material debemos aplicar la siguiente regla css:
:host ::ng-deep .mat-horizontal-step...]]></description><link>https://devblog.stalinmaza.com/angular-solucion-a-problemas-comunes</link><guid isPermaLink="true">https://devblog.stalinmaza.com/angular-solucion-a-problemas-comunes</guid><category><![CDATA[Angular]]></category><category><![CDATA[angular material]]></category><category><![CDATA[RxJS]]></category><dc:creator><![CDATA[Stalin Maza]]></dc:creator><pubDate>Sat, 22 Feb 2020 02:56:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1599931273941/NOGZeJ2im.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-esconder-el-header-del-mat-stepper-de-angular-material">Esconder el header del mat-stepper de Angular Material</h3>
<p>En caso de que necesitemos ocultar la cabecera donde se muestran los pasos del stepper que nos ofrece angular material debemos aplicar la siguiente regla css:</p>
<pre><code class="lang-css"><span class="hljs-selector-pseudo">:host</span> <span class="hljs-selector-pseudo">::ng-deep</span> <span class="hljs-selector-class">.mat-horizontal-stepper-header-container</span> { <span class="hljs-attribute">display</span>: none; } *<span class="hljs-selector-tag">esconder</span> <span class="hljs-selector-tag">header</span> <span class="hljs-selector-tag">de</span> <span class="hljs-selector-tag">angular</span> <span class="hljs-selector-tag">material</span>*
</code></pre>
<h3 id="heading-obtener-lenguaje-que-tiene-configurado-el-usuario">Obtener lenguaje que tiene configurado el usuario</h3>
<p>En caso de que necesitemos obtener dinámicamente el lenguaje que tiene el usuario para así poder mostrar el sitio en el idioma solicitado, podemos aplicar la siguiente función:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> obtenerLenguajeNavegador = <span class="hljs-function">() =&gt;</span> { <span class="hljs-keyword">return</span> <span class="hljs-built_in">window</span>.navigator.languages ? <span class="hljs-built_in">window</span>.navigator.languages[<span class="hljs-number">0</span>] : <span class="hljs-literal">null</span>;
    lang = lang || <span class="hljs-built_in">window</span>.navigator.language || <span class="hljs-built_in">window</span>.navigator.browserLanguage || <span class="hljs-built_in">window</span>.navigator.userLanguage; }
</code></pre>
<h3 id="heading-detectar-si-la-aplicacion-tiene-acceso-a-internet">Detectar si la aplicación tiene acceso a internet</h3>
<p>Para poder realizar este ejercicio se debe utilizar el evento 'online' y 'offline' del objeto global window. Además se aprovecha las bondades que ofrece la programación reactiva con RXJS para poder elaborar un observable al cual nos vamos a poder subscribir y así enterarnos cuando el evento se ejecute. Por lo cual vamos a crear un servicio de Angular llamado 'network.service.ts"</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Injectable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-comment">//Importamos los operadores y funciones de RXJS</span>
<span class="hljs-keyword">import</span> { Observable, fromEvent, merge, <span class="hljs-keyword">of</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'rxjs'</span>;
<span class="hljs-keyword">import</span> { mapTo, share } <span class="hljs-keyword">from</span> <span class="hljs-string">'rxjs/operators'</span>;

<span class="hljs-meta">@Injectable</span>({
    providedIn: <span class="hljs-string">'root'</span>
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> NetworkService {

<span class="hljs-comment">//Se crea un observable que sera de tipo booleano</span>
statusNetwork$: Observable&lt;<span class="hljs-built_in">boolean</span>&gt;;

<span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) {
  <span class="hljs-comment">//En el constructor de la clase inicializamos la </span>
<span class="hljs-comment">//asignación del evento al observable que creamos anteriormente.</span>
<span class="hljs-comment">//Por lo cual hacemos uso del operador 'fromEvent' </span>
<span class="hljs-comment">//para estar pendientes de la ejecución del evento 'offline' y 'online' </span>
<span class="hljs-comment">//y de acuerdo a eso devolver un valor de 'True' o 'False'</span>

  <span class="hljs-built_in">this</span>.statusNetwork$ = merge(
    <span class="hljs-keyword">of</span>(navigator.onLine),
    fromEvent(<span class="hljs-built_in">window</span>, <span class="hljs-string">'online'</span>).pipe(mapTo(<span class="hljs-literal">true</span>)),
    fromEvent(<span class="hljs-built_in">window</span>, <span class="hljs-string">'offline'</span>).pipe(mapTo(<span class="hljs-literal">false</span>))
  )

  }

isOnline(): Observable&lt;<span class="hljs-built_in">boolean</span>&gt;{
    <span class="hljs-comment">//Se utiliza el operador 'share' para que todos los</span>
   <span class="hljs-comment">//que se subscriban a este observable obtengan el mismo valor</span>
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.statusNetwork$.pipe(share());
}

}
</code></pre>
<p>Una vez creado el servicio solo debemos llamarlo en alguno o varios de los componentes donde lo necesitemos, por ejemplo:</p>
<pre><code class="lang-xml">import { Component, OnInit } from '@angular/core';
import { NetworkService } from 'src/app/services/network.service';

@Component({
    selector: 'app-root',
    templateUrl: 'app.component.html',
    styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {

    isConnected = false;

    constructor(
        private networkService: NetworkService,
    ) {
    }
    ngOnInit(): void {
        this.networkService.isOnline().subscribe(online =&gt; this.isConnected = online);
    }

}
</code></pre>
<p>y en el HTML mostramos un mensaje dependiendo si tenemos conexión a internet o no.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"alert alert-success"</span> *<span class="hljs-attr">ngIf</span>=<span class="hljs-string">"isConnected"</span>&gt;</span>
Usted cuenta con una conexión a internet activa
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"alert alert-danger"</span> *<span class="hljs-attr">ngIf</span>=<span class="hljs-string">"!isConnected"</span>&gt;</span>
No existe disponibilidad de acceso a internet, por favor revise su conexión
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Xamarin, cosas útiles que deberías conocer]]></title><description><![CDATA[Existen muchos frameworks para desarrollar aplicaciones móviles, con swift o java para aplicaciones nativas, flutter para aplicaciones nativas de un gran performance, react native, ionic o xamarin como aplicaciones hibridas, sea cualquiera la que eli...]]></description><link>https://devblog.stalinmaza.com/xamarin-cosas-utiles-que-deberias-conocer</link><guid isPermaLink="true">https://devblog.stalinmaza.com/xamarin-cosas-utiles-que-deberias-conocer</guid><category><![CDATA[Xamarin]]></category><category><![CDATA[Microsoft]]></category><dc:creator><![CDATA[Stalin Maza]]></dc:creator><pubDate>Thu, 20 Feb 2020 03:07:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1595477337463/4efE5R85J.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Existen muchos frameworks para desarrollar aplicaciones móviles, con swift o java para aplicaciones nativas, flutter para aplicaciones nativas de un gran performance, react native, ionic o xamarin como aplicaciones hibridas, sea cualquiera la que elijas, siempre va a haber dudas comunes que tenemos al programar.</p>
<p>En esta publicación se irán añadiendo soluciones a problemas o dudas comunes al manejar Xamarin, para que si necesitas recordar como se hacia alguna cosa, puedes revisar este post y no perder mucho tiempo buscando en Google y perdiendo tiempo entre tantas respuestas que surgen, que son útiles pero que nos demoramos mucho hasta encontrar la correcta.</p>
<p>Sin más que acotar, los dejo con el listado de dudas - problemas comunes y sus soluciones.</p>
<h3 id="error-mono-androidtools-requiresuninstallexception-">Error Mono.AndroidTools.RequiresUninstallException:</h3>
<p>Este error suele suceder al cambiar de versión de SDK que se esta utilizando, y lo que quiere decir es que la herramienta Mono que se ha instalado en el dispositivo donde queremos desplegar la aplicación no es compatible con la versión actual de SDK que se esta utilizando.</p>
<p>La solución es desinstalar el apk de esta herramienta, pero vía dispositivo es muy dificil, por lo que debemos conectar el dispositivo y activar el modo de depuración.</p>
<p>Luego debemos abrir el menu <strong>Tools</strong> en la barra de herramientas de Visual Studio  y luego seleccionamos la plataforma que estamos usand, en este caso <strong>Android</strong> y escogemos la opción de lanzar el <strong>adb command promt</strong>.</p>
<p>Una vez abierta la terminal debemos ejecutar el siguiente comando:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Ejecutar el comando de desinstalación via ADB</span>
adb uninstall com.nombrecompañia.nombreaplicacion
</code></pre>
<h3 id="autor">Autor</h3>
<blockquote>
<p>Stalin Maza - Desarrollador Web - Móvil</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Comandos Útiles Git]]></title><description><![CDATA[Git es una grandiosa herramienta que nos permite realizar un control de versiones profesional sobre nuestros proyectos y saber manejarla es un plus muy importante, en este post resumo algunos puntos importantes que me han servido a lo largo de mi car...]]></description><link>https://devblog.stalinmaza.com/comandos-utiles-git</link><guid isPermaLink="true">https://devblog.stalinmaza.com/comandos-utiles-git</guid><category><![CDATA[Git]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[GitLab]]></category><dc:creator><![CDATA[Stalin Maza]]></dc:creator><pubDate>Sat, 08 Feb 2020 04:10:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1576034089466/HjDiUZjDN.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Git es una grandiosa herramienta que nos permite realizar un control de versiones profesional sobre nuestros proyectos y saber manejarla es un plus muy importante, en este post resumo algunos puntos importantes que me han servido a lo largo de mi carrera como Desarrollador Web.</p>
<h3 id="configuraciones-globales">Configuraciones globales</h3>
<p>Para realizar configuraciones ejecutamos el comando &quot;git config --global&quot;, por ejemplo para establecer el usuario y el correo del usuario global en tu pc o laptop debes ejecutar:</p>
<pre><code><span class="hljs-comment">### CONFIGURACION GLOBAL</span>
git config --<span class="hljs-keyword">global</span> user.name <span class="hljs-string">"YOUR_USER_NAME"</span>
git config --<span class="hljs-keyword">global</span> user.email <span class="hljs-string">"YOUR_USER_EMAIL"</span>
</code></pre><h3 id="inicializar-un-repositorio">Inicializar un repositorio</h3>
<p>Para inicializar un repositorio en el directorio actual ejecutamos:</p>
<pre><code class="lang-bash">git init
</code></pre>
<h3 id="a-adir-archivos-al-stage">Añadir archivos al stage</h3>
<p>Para añadir archivos al stage y github les mantenga seguimiento ejecutamos el comando:</p>
<pre><code class="lang-bash">git add file_name // archivo en especifico
git add . // todos los archivos
</code></pre>
<h3 id="cambiar-de-rama">Cambiar de Rama</h3>
<p>Si estas trabajando en una rama y deseas cambiar a otra y crearla al mismo tiempo puedes ejecutar el comando:</p>
<pre><code class="lang-bash">git checkout -b branch_name
</code></pre>
<h3 id="hacer-un-commit">Hacer un commit</h3>
<p>Si estas trabajando con archivos en el stage y deseas hacer un commit debes ejecutar el comando:</p>
<pre><code class="lang-bash">git commit -m <span class="hljs-string">"message_to_commit"</span>
</code></pre>
<h3 id="mostrar-origenes-locales-y-remotos-del-repositorio">Mostrar origenes locales y remotos del repositorio</h3>
<p>Si estas trabajando con archivos en el stage y deseas hacer un commit debes ejecutar el comando:</p>
<pre><code class="lang-bash">git remote show origin
</code></pre>
<h3 id="remover-archivos">Remover archivos</h3>
<p>En git para remover archivos es similar a como se trabaja en Linux</p>
<pre><code><span class="hljs-attribute">git</span> rm ‘.txt’  <span class="hljs-comment">#remover un archivo en especifico</span>
git rm -r ‘doodle’  <span class="hljs-comment">#remover carpetas</span>
git rm -r --cached . <span class="hljs-comment"># remover archivos cache</span>
</code></pre><h3 id="listar-diferencias">Listar diferencias</h3>
<p>Para poder listar las diferencias o cambios entre el repositorio remoto y el local podemos ejecutar el comando:</p>
<pre><code><span class="hljs-attribute">git</span> diff<span class="hljs-meta"> [filename]</span>
</code></pre><h3 id="crear-alias-de-comandos">Crear alias de comandos</h3>
<p>Si deseas ver de una mejor manera el log de tu repositorio puedes crear el siguiente alias</p>
<pre><code class="lang-bash">git config --global alias.lg <span class="hljs-string">"log --oneline --decorate --all --graph"</span>
</code></pre>
<p>Con esto para ver el log del repositorio solo debes ejecutar &quot;git lg&quot;</p>
<p>Si deseas listar de una forma amigable los archivos que estan en el stage o los que no están puedes crear el alias</p>
<pre><code class="lang-bash">git config --global alias.s <span class="hljs-string">"status -s -b"</span>
</code></pre>
<p>Con esto para ver el log del repositorio solo debes ejecutar &quot;git s&quot;</p>
<h3 id="crear-etiquetas-tags-">Crear etiquetas(tags)</h3>
<p>Para crear etiquetas, por ejemplo para mantener un versionado del proyecto debemos ejecutar el comando:</p>
<pre><code>git tag -a v0.0.1 #<span class="hljs-keyword">commit</span> -<span class="hljs-keyword">m</span> <span class="hljs-string">"message"</span> //reemplazas v<span class="hljs-number">.0</span><span class="hljs-number">.0</span><span class="hljs-number">.1</span> por tu versió<span class="hljs-keyword">n</span>
</code></pre><p>Para subir las etiquetas a Github debes eejecutar el siguiente comando, ya que por defecto no se suben al ejecutar un git push</p>
<pre><code><span class="hljs-attribute">git</span> push --tags
</code></pre><h3 id="clonar-repositorio">Clonar Repositorio</h3>
<p>Para clonar un repositorio lo hacemos de la siguiente manera, especificando la url del repositorio y opcionalmente la rama que deseamos clonar </p>
<pre><code>git <span class="hljs-built_in">clone</span> [branch_name] url_repositorio_remoto directorio_<span class="hljs-built_in">local</span>_donde_guardar
</code></pre><h3 id="bajar-cambios-de-un-repositorio-remoto">Bajar cambios de un repositorio remoto</h3>
<p>Podemos utilizar el comando</p>
<pre><code><span class="hljs-selector-tag">git</span> <span class="hljs-selector-tag">pull</span> <span class="hljs-selector-tag">origin</span> <span class="hljs-selector-attr">[branch_name]</span> <span class="hljs-selector-attr">[--allow-unrelated-histories]</span>
</code></pre><p>el cual descarga los cambios remotos e inmediatamente intenta hacer un merge con los cambios locales. Si no deseas que suceda esto puedes usar los siguientes comandos:</p>
<pre><code><span class="hljs-selector-tag">git</span> <span class="hljs-selector-tag">fetch</span> <span class="hljs-selector-tag">origin</span> <span class="hljs-selector-attr">[branch_name]</span> <span class="hljs-selector-attr">[--all]</span>
</code></pre><p>el cual se descarga los cambios remotos, y luego con el comando:</p>
<pre><code><span class="hljs-attribute">git</span> merge
</code></pre><p>realizamos manualmente la fusión(merge), si da errores de conflictos revisamos los archivos que dieron conflicto y podemos aceptar los dos cambios, aceptar nuestros cambios o aceptar los cambios que vienen del repositorio remoto</p>
<h3 id="regresar-al-ultimo-commit-y-descartar-nuevos-cambios">Regresar al ultimo commit y descartar nuevos cambios</h3>
<p>Para resetear nuestro repositorio al ultimo commit debemos ejecutar el siguiente comando:</p>
<pre><code>git <span class="hljs-keyword">reset</span> <span class="hljs-comment">--hard origin/[branchname]</span>
</code></pre><h3 id="setear-una-rama-local-a-una-rama-remota">Setear una rama local a una rama remota</h3>
<p>Si creamos una rama local y queremos que git sepa a que rama nos referimos cuando queremos hacer un git push o git fetch, debemos ejecutar el siguiente comando:</p>
<pre><code><span class="hljs-attribute">git</span> branch --set-upstream-to=origin/[branch_local_name]<span class="hljs-meta"> [branch_remote_name]</span>
</code></pre>]]></content:encoded></item><item><title><![CDATA[Ejercicios Prácticos con Hosting Online]]></title><description><![CDATA[Muchas veces he tenido que hacer bastantes ejercicios sobre Javascript y necesito mostrarle después a los compañeros o a las personas que debo enseñarles y así poder compartirles mis conocimientos a los demás.
Para eso suelo utilizar la plataforma Ne...]]></description><link>https://devblog.stalinmaza.com/ejercicios-practicos-con-hosting-online</link><guid isPermaLink="true">https://devblog.stalinmaza.com/ejercicios-practicos-con-hosting-online</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[Ionic Framework]]></category><category><![CDATA[jQuery]]></category><dc:creator><![CDATA[Stalin Maza]]></dc:creator><pubDate>Tue, 04 Feb 2020 03:43:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1580787805507/Ftm-JXiZc.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Muchas veces he tenido que hacer bastantes ejercicios sobre Javascript y necesito mostrarle después a los compañeros o a las personas que debo enseñarles y así poder compartirles mis conocimientos a los demás.</p>
<p>Para eso suelo utilizar la plataforma Netlify donde puede hostear mis ejercicios y con la integración de Github puedo guardar el código y cualquier actualización se subirá y se actualizara al instante.</p>
<p>Para esto tengo los siguientes links disponibles:</p>
<ul>
<li>Ejercicios <a target='_blank' rel='noopener noreferrer'  href="https://stalinmazapj97techjs.netlify.com/">Javascript</a> </li>
<li>Ejercicio JavaScript con <a target='_blank' rel='noopener noreferrer'  href="https://proyectosmtech97s.netlify.com/">HTTPS</a></li>
<li>Curriculum <a target='_blank' rel='noopener noreferrer'  href="https://stalinmazaepn.github.io/stalinMaza/">Online</a></li>
<li>Ejercicio App de Noticias con <a target='_blank' rel='noopener noreferrer'  href="https://top-news-stalin-sm-maza.netlify.com/">JQuery</a></li>
<li>Ejercicio Grilla <a target='_blank' rel='noopener noreferrer'  href="https://grillasmtohexepn.netlify.com/">Hexadecimal</a></li>
<li>Ejercicios CSS/JS con <a target='_blank' rel='noopener noreferrer'  href="https://codepen.io/stalinmaza97">Codepen</a></li>
</ul>
<h3 id="autor">Autor</h3>
<blockquote>
<p>Stalin Maza</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Expresiones Regulares Útiles]]></title><description><![CDATA[Muchas veces para realizar validaciones sean en formularios o en cualquier otra parte que se necesite usamos expresiones regulares y en este post quiero recopilar las más útiles y que me han ayudado mucho a lo largo de mi carrera como Desarrollador.
...]]></description><link>https://devblog.stalinmaza.com/expresiones-regulares-utiles</link><guid isPermaLink="true">https://devblog.stalinmaza.com/expresiones-regulares-utiles</guid><dc:creator><![CDATA[Stalin Maza]]></dc:creator><pubDate>Tue, 10 Dec 2019 16:09:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1575994117936/72nSwnGDm.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Muchas veces para realizar validaciones sean en formularios o en cualquier otra parte que se necesite usamos expresiones regulares y en este post quiero recopilar las más útiles y que me han ayudado mucho a lo largo de mi carrera como Desarrollador.</p>
<h3 id="validar-solo-d-gitos-de-un-tama-o-de-4-6-8">Validar Solo Dígitos de un tamaño de 4, 6 ó 8</h3>
<pre><code>^(\d{4}|\d{6}|\d{8})?$
</code></pre><h3 id="validar-email">Validar Email</h3>
<pre><code><span class="hljs-regexp">/[A-Z0-9._%+-]+@[A-Z0-9-]+.+.[A-Z]{2,4}/igm</span>
</code></pre><h3 id="validar-url">Validar URL</h3>
<pre><code><span class="hljs-regexp">/^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/</span>
</code></pre><h3 id="validar-contrase-as-seguras">Validar Contraseñas Seguras</h3>
<pre><code>(?=^.{8,}$)((?=.<span class="hljs-emphasis">*\d)|(?=.*</span>\W+))(?![.\n])(?=.<span class="hljs-emphasis">*[A-Z])(?=.*</span>[a-z]).*$
</code></pre><h3 id="validar-n-mero-de-tarjetas-de-cr-dito">Validar Número de Tarjetas de Crédito</h3>
<pre><code>^((<span class="hljs-number">67</span>\d{<span class="hljs-number">2</span>})|(<span class="hljs-number">4</span>\d{<span class="hljs-number">3</span>})|(<span class="hljs-number">5</span>[<span class="hljs-number">1</span>-<span class="hljs-number">5</span>]\d{<span class="hljs-number">2</span>})|(<span class="hljs-number">6011</span>))(-<span class="hljs-string">?\s</span>?\d{<span class="hljs-number">4</span>}){<span class="hljs-number">3</span>}|(<span class="hljs-number">3</span>[<span class="hljs-number">4</span>,<span class="hljs-number">7</span>])\ d{<span class="hljs-number">2</span>}-<span class="hljs-string">?\s</span>?\d{<span class="hljs-number">6</span>}-<span class="hljs-string">?\s</span>?\d{<span class="hljs-number">5</span>}$
</code></pre>]]></content:encoded></item><item><title><![CDATA[Angular Leaflet Routing]]></title><description><![CDATA[Para este ejemplo debemos primero instalar lo necesario para poder trabajar con Leaflet en Angular o Ionic + Angular
npm install leaflet leaflet-routing-machine leaflet-gesture-handling

Añadimos el CSS necesario en el archivo index.html
<link rel="s...]]></description><link>https://devblog.stalinmaza.com/angular-leaflet-routing</link><guid isPermaLink="true">https://devblog.stalinmaza.com/angular-leaflet-routing</guid><category><![CDATA[Angular]]></category><category><![CDATA[maps]]></category><category><![CDATA[ionic]]></category><dc:creator><![CDATA[Stalin Maza]]></dc:creator><pubDate>Fri, 01 Nov 2019 22:28:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1576034719647/MKsrrgypY.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Para este ejemplo debemos primero instalar lo necesario para poder trabajar con Leaflet en Angular o Ionic + Angular</p>
<pre><code class="lang-bash">npm install leaflet leaflet-routing-machine leaflet-gesture-handling
</code></pre>
<p>Añadimos el CSS necesario en el archivo index.html</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"./assets/css/leaflet-gesture-handling.min.css"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text/css"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"./assets/css/leaflet-search.css"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text/css"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"./assets/css/leaflet-distance-marker.css"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text/css"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"./assets/css/leaflet-routing-machine.css"</span> /&gt;</span>
</code></pre>
<p>Y luego del body añadimos los scripts externos, esto porque al usar los paquetes npm de estas librerias no me funcionaba por lo que la version cdn si funciona.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"./assets/js/leaflet-geometryutil.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"./assets/js/leaflet-distance-marker.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>Hecho esto creamos un componente con el comando:</p>
<pre><code class="lang-bash">ng generate component simpleRouting
</code></pre>
<p>Luego de eso en el archivo HTML creamos un elemento "div" que contenga el mapa.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> [<span class="hljs-attr">id</span>]=<span class="hljs-string">"id"</span> [<span class="hljs-attr">class</span>]=<span class="hljs-string">"className"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>Ahora si podemos concentrarnos en el archivo typescript Primero debemos importar todo lo necesario para el ejercicio, en este caso las librerias de leaflet, un servicio para obtener la ubicación actual, un servicio que entrega un marcador personalizado, y una interfaz para el tipado de typescript.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-string">'leaflet'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'leaflet-routing-machine'</span>;
<span class="hljs-keyword">import</span> { Component, OnInit, Input } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { MapService } <span class="hljs-keyword">from</span> <span class="hljs-string">"src/app/services/map.service"</span>;
<span class="hljs-keyword">import</span> { LocalizationService } <span class="hljs-keyword">from</span> <span class="hljs-string">"src/app/services/localization.service"</span>;
<span class="hljs-keyword">import</span> { GestureHandling } <span class="hljs-keyword">from</span> <span class="hljs-string">"leaflet-gesture-handling"</span>;
<span class="hljs-keyword">import</span> { environment } <span class="hljs-keyword">from</span> <span class="hljs-string">'src/environments/environment'</span>;
<span class="hljs-keyword">import</span> { IUbication } <span class="hljs-keyword">from</span> <span class="hljs-string">"src/app/interfaces/models"</span>;

<span class="hljs-keyword">declare</span> <span class="hljs-keyword">let</span> L: <span class="hljs-built_in">any</span>;

<span class="hljs-meta">@Component</span>({
    selector: <span class="hljs-string">'simple-routing-map'</span>,
    templateUrl: <span class="hljs-string">'./simple-routing-map.component.html'</span>,
    styleUrls: [<span class="hljs-string">'./simple-routing-map.component.scss'</span>],
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> SimpleRoutingMapComponent <span class="hljs-keyword">implements</span> OnInit {
</code></pre>
<p>Hecho esto en la clase especificamos algunos parámetros o propiedades que tendrá el componente como el id, la clase, el nivel de zoom, las coordenadas de destino, habilitar los gestos del mapa y la opción de manejar la ruta con una polilinea o con leaflet routing machine</p>
<pre><code class="lang-typescript">    <span class="hljs-meta">@Input</span>() id: <span class="hljs-built_in">string</span>;
    <span class="hljs-meta">@Input</span>() className = <span class="hljs-string">''</span>;
    <span class="hljs-meta">@Input</span>() zoom = <span class="hljs-number">16</span>;
    <span class="hljs-meta">@Input</span>() destinationCoords: IUbication;
    <span class="hljs-meta">@Input</span>() enableGesture = <span class="hljs-literal">false</span>;
    <span class="hljs-meta">@Input</span>() usePolyline = <span class="hljs-literal">true</span>;
</code></pre>
<p>Hecho esto las demas variables que usamos son para guardar la polilinea, el mapa, la capa de marcadores, la coordenada actual, el array de coordenadas para el enrutamiento y para saber si el mapa ya cargo.</p>
<pre><code class="lang-typescript">    polylineRoute: <span class="hljs-built_in">any</span>;
    map: <span class="hljs-built_in">any</span>;
    mapMarkers: <span class="hljs-built_in">any</span>[] = <span class="hljs-literal">null</span>;
    mapIsLoaded = <span class="hljs-literal">false</span>;
    markersLayer = <span class="hljs-keyword">new</span> L.LayerGroup();
    currentCoordinate: <span class="hljs-built_in">any</span> = <span class="hljs-literal">null</span>;
    routeControl: <span class="hljs-built_in">any</span>;
    arrRoutesLatLng = [];

    <span class="hljs-keyword">constructor</span>(<span class="hljs-params">
        <span class="hljs-keyword">private</span> mapService: MapService,
        <span class="hljs-keyword">private</span> localizationService: LocalizationService
    </span>) {

    }

    <span class="hljs-keyword">async</span> ngOnInit() { }
</code></pre>
<p>Ejecutamos el código en el ciclo "AfterViewInit" para evitar problemas de renderización. Primero obtenemos los marcadores disponibles, luego esperamos que el servicio de localización me devuelva mis coordenadas actuales y al final inicializamos el mapa.</p>
<pre><code class="lang-typescript">    <span class="hljs-keyword">async</span> ngAfterViewInit() {
        <span class="hljs-comment">// Obtener marcadores</span>
        <span class="hljs-built_in">this</span>.mapMarkers = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.mapService.getMarkers().toPromise();
        <span class="hljs-comment">// Obtener Coordenadas</span>
        <span class="hljs-built_in">this</span>.currentCoordinate = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.localizationService.getCoordinate();
        <span class="hljs-comment">// Inicializar el Mapa</span>
        <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.initializeMap();
    }
</code></pre>
<p>En esta función habilitamos/deshabilitamos los gestos del mapa. Luego seteamos el valor de las coordenadas para el enrutamiento, creamos el mapa, le agregamos algunos eventos y configuramos la capa del Mapa. Luego añadimos los marcadores en el mapa y al final realizamos el enrutamiento sea con una polilinea o con el routing de Leaflet Routing Machine.</p>
<pre><code class="lang-typescript">    <span class="hljs-keyword">async</span> initializeMap() {
        <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.enableGesture) {
            L.Map.addInitHook(<span class="hljs-string">"addHandler"</span>, <span class="hljs-string">"gestureHandling"</span>, GestureHandling);
        }
        <span class="hljs-comment">//Setear las Coordenadas de tipo LatLng</span>
        <span class="hljs-built_in">this</span>.arrRoutesLatLng[<span class="hljs-number">0</span>] = <span class="hljs-built_in">this</span>.createLatLng(<span class="hljs-built_in">this</span>.currentCoordinate.latitude, <span class="hljs-built_in">this</span>.currentCoordinate.longitude);
        <span class="hljs-built_in">this</span>.arrRoutesLatLng[<span class="hljs-number">1</span>] = <span class="hljs-built_in">this</span>.createLatLng(<span class="hljs-built_in">this</span>.destinationCoords.latitude, <span class="hljs-built_in">this</span>.destinationCoords.longitude);
        <span class="hljs-comment">// Crear el Mapa</span>
        <span class="hljs-built_in">this</span>.map = L.map(<span class="hljs-built_in">this</span>.id, {
            gestureHandling: <span class="hljs-built_in">this</span>.enableGesture,
            zoomAnimation: <span class="hljs-literal">true</span>,
            markerZoomAnimation: <span class="hljs-literal">true</span>,
            zoomControl: <span class="hljs-literal">true</span>
        });
        <span class="hljs-comment">// Agregar Evento al Mapa cuando esta cargado</span>
        <span class="hljs-built_in">this</span>.map.on(<span class="hljs-string">'load'</span>, <span class="hljs-function">(<span class="hljs-params">e: <span class="hljs-built_in">any</span></span>) =&gt;</span> {
            <span class="hljs-built_in">this</span>.mapIsLoaded = <span class="hljs-literal">true</span>;
            <span class="hljs-comment">// Invalidar Tamanio</span>
            <span class="hljs-built_in">this</span>.map.invalidateSize();
        });
        <span class="hljs-built_in">this</span>.map.zoomControl.setPosition(<span class="hljs-string">'topright'</span>);
        <span class="hljs-comment">// Configurar la vista centrada</span>
        <span class="hljs-built_in">this</span>.map.setView([<span class="hljs-number">-0.1548643</span>, <span class="hljs-number">-78.4822049</span>], <span class="hljs-built_in">this</span>.zoom);
        <span class="hljs-comment">// Agregar la capa del Mapa</span>
        L.tileLayer(environment.mapLayers.google.url, {
            attribution: environment.mapLayers.google.attribution,
            maxZoom: <span class="hljs-number">18</span>,
            updateWhenIdle: <span class="hljs-literal">true</span>,
            reuseTiles: <span class="hljs-literal">true</span>
        }).addTo(<span class="hljs-built_in">this</span>.map);
        <span class="hljs-comment">//Añadir la Ruta en caso de ser necesario</span>
        <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">this</span>.usePolyline) {
            <span class="hljs-built_in">this</span>.setRoutingMachine(<span class="hljs-built_in">this</span>.arrRoutesLatLng);
        }
        <span class="hljs-built_in">this</span>.map.addLayer(<span class="hljs-built_in">this</span>.markersLayer);
        <span class="hljs-comment">// Si obtuve coordenadas añadir el marcador</span>
        <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.currentCoordinate) {
            <span class="hljs-keyword">const</span> iconCurrent = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.mapService.getCustomIcon(<span class="hljs-string">'red'</span>);
            <span class="hljs-keyword">let</span> currentPoint: <span class="hljs-built_in">any</span>;
            <span class="hljs-built_in">this</span>.arrRoutesLatLng[<span class="hljs-number">0</span>] = <span class="hljs-built_in">this</span>.createLatLng(<span class="hljs-built_in">this</span>.currentCoordinate.latitude, <span class="hljs-built_in">this</span>.currentCoordinate.longitude);
            <span class="hljs-keyword">if</span> (iconCurrent) {
                currentPoint = <span class="hljs-keyword">new</span> L.Marker(<span class="hljs-built_in">this</span>.arrRoutesLatLng[<span class="hljs-number">0</span>], { icon: iconCurrent, title: <span class="hljs-string">'Mi Posición Actual'</span> });
            } <span class="hljs-keyword">else</span> {
                currentPoint = <span class="hljs-keyword">new</span> L.Marker(<span class="hljs-built_in">this</span>.arrRoutesLatLng[<span class="hljs-number">0</span>], { title: <span class="hljs-string">'Mi Posición Actual'</span> });
            }
            currentPoint.bindPopup(<span class="hljs-string">'Mi Ubicación Actual'</span>).openPopup();
            <span class="hljs-built_in">this</span>.markersLayer.addLayer(currentPoint);
        }
        <span class="hljs-comment">//Añadir el destino final</span>
        <span class="hljs-keyword">let</span> punto = <span class="hljs-literal">null</span>;
        <span class="hljs-keyword">const</span> markerIcon = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.mapService.getCustomIcon(<span class="hljs-string">'green'</span>);

        <span class="hljs-keyword">if</span> (markerIcon) {
            punto = <span class="hljs-keyword">new</span> L.Marker(<span class="hljs-built_in">this</span>.arrRoutesLatLng[<span class="hljs-number">1</span>], { icon: markerIcon });
        } <span class="hljs-keyword">else</span> {
            punto = <span class="hljs-keyword">new</span> L.Marker(<span class="hljs-built_in">this</span>.arrRoutesLatLng[<span class="hljs-number">1</span>]);
        }
        <span class="hljs-comment">// Añadir el punto a la capa de marcadores</span>
        <span class="hljs-built_in">this</span>.markersLayer.addLayer(punto);
        <span class="hljs-comment">//Añado la polilinea de ser necesario</span>
        <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.usePolyline) {
            <span class="hljs-built_in">this</span>.addPolyline(<span class="hljs-built_in">this</span>.arrRoutesLatLng);
        }

    }
</code></pre>
<p>Esta función sirve para añadir la polilinea al mapa pasandole como parámetros el array de coordenadas y al final centramos el mapa.</p>
<pre><code class="lang-typescript">    addPolyline(arrayCoordsLatLng: <span class="hljs-built_in">any</span>) {
        <span class="hljs-built_in">this</span>.polylineRoute = L.polyline(arrayCoordsLatLng,
            {
                color: <span class="hljs-string">'#ee0033'</span>,
                weight: <span class="hljs-number">8</span>,
                opacity: <span class="hljs-number">.8</span>,
                dashArray: <span class="hljs-string">'20,15'</span>,
                lineJoin: <span class="hljs-string">'round'</span>
            }
        );
        <span class="hljs-comment">//Añadir Ruta Polyline</span>
        <span class="hljs-built_in">this</span>.markersLayer.addLayer(<span class="hljs-built_in">this</span>.polylineRoute);
        <span class="hljs-built_in">this</span>.map.fitBounds(<span class="hljs-built_in">this</span>.polylineRoute.getBounds());
    }
</code></pre>
<p>Esta función añade la ruta con Leaflet Routing Machine usando como Router el Api de Mapbox para evitar el limite de ORSM.</p>
<pre><code class="lang-typescript">    setRoutingMachine(arrCoords: <span class="hljs-built_in">any</span>) {
        <span class="hljs-built_in">this</span>.routeControl = L.Routing.control({
            waypoints: arrCoords,
            show: <span class="hljs-literal">false</span>,
            routeWhileDragging: <span class="hljs-literal">false</span>,
            router: <span class="hljs-keyword">new</span> L.Routing.mapbox(environment.mapBoxApiKey),
            createMarker: <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{ <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>; }
        });
        <span class="hljs-built_in">this</span>.routeControl.addTo(<span class="hljs-built_in">this</span>.map);
    }
</code></pre>
<p>Esta función sirve para actualizar las coordenadas de la ruta.</p>
<pre><code class="lang-typescript">    updateRouteCoords(arrCoords: <span class="hljs-built_in">any</span>[]) {
        <span class="hljs-built_in">this</span>.routeControl.setWaypoints(arrCoords);
    }
</code></pre>
<p>Esta función crea un array LatLng con Leaflet.</p>
<pre><code class="lang-typescript">    createLatLng(latitude: <span class="hljs-built_in">number</span>, longitude: <span class="hljs-built_in">number</span>) {
        <span class="hljs-keyword">return</span> L.latLng(latitude, longitude);
    } 
}
</code></pre>
<h3 id="heading-nota">Nota</h3>
<p>Si desean probar este código pronto espero subirlo en <a target="_blank" href="https://stackblitz.com/">Stackblitz</a> pero ustedes mismo pueden probarlo creando un entorno, es gratis y sirve mucho para no usar los recursos de su PC.</p>
<p>Para llamar al componente lo haces de la siguiente manera:</p>
<pre><code class="lang-typescript"> &lt;simple-routing-map [zoom]=<span class="hljs-string">"11"</span> className=<span class="hljs-string">"public-service-map"</span>
            id=<span class="hljs-string">"public-service-map-detail"</span>
            [destinationCoords]=<span class="hljs-string">"coordsArr"</span> [usePolyline]=<span class="hljs-string">"false"</span>&gt;&lt;/simple-routing-map&gt;
</code></pre>
<h3 id="heading-imagenes">Imágenes</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1572646984921/JfEqpvCLo.jpeg" alt="map_routing_polyline.jpg" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1572647089061/4XvDxCLyt.jpeg" alt="map_routing_machine.jpg" /></p>
]]></content:encoded></item></channel></rss>