Saltar a contenido

Otro framework

React y Svelte tienen su adaptador generado, pero el cliente no está atado a ellos: los dos están hechos sobre el núcleo, que no depende de nada, y cualquier otro framework de TypeScript —Vue, Solid, Angular, Lit o ninguno— puede usar el núcleo tal cual. Esta página es el contrato: lo que da el núcleo y lo que tiene que hacer un adaptador.

Lo que da el núcleo

Qué De dónde Para qué
createTaiClient() ./api Las llamadas, la sesión y los tipos
api.session.restore() Recuperar la sesión al arrancar
api.session.subscribe(fn) Avisa con el estado ahora y en cada cambio; devuelve la función para dejar de escuchar. Es el contrato de store de Svelte, y lo que necesita useSyncExternalStore de React
api.session.getSnapshot() / .state El estado actual
sessionView(api.session, estado) ./api La vista de la sesión de una pantalla: user, loading, isAuthenticated, login, logout, hasPermission…
hasPermission(user, p), can(user, tabla, acción) ./api Los permisos, como funciones puras del usuario
defineRoutes, resolveRoutes ./api/routes La declaración de pantallas, y la misma con lo heredado ya aplicado
access(resueltas, pathname, vista) ./api/routes Qué hacer en una URL: loading, login, underConstruction, forbidden o allowed
navigation, breadcrumbs, currentRoute ./api/routes El menú, las migas y la pantalla actual
safeReturnPath(from) ./api/routes Adónde volver tras el login, sin salir del sitio

Son las mismas funciones que usan por dentro react/ y svelte/: un adaptador nuevo decide lo mismo que los otros dos con la misma declaración.

Lo que hace un adaptador

Tres cosas, y ninguna más:

  1. Dar el cliente a los componentes y recuperar la sesión al arrancar. En React es un provider; en Svelte, el contexto; en otros, una inyección de dependencias o un módulo con la instancia.
  2. Convertir la sesión en estado del framework: escuchar con api.session.subscribe y entregar sessionView(api.session, estado) cada vez que cambia.
  3. Llevar la regla de access() al router: antes de entrar en una URL —y otra vez cuando cambia la sesión—, ir al login, enseñar la 404 o «en construcción», o dejar pasar. Y recalcular el menú y las migas cuando cambian la sesión o la URL.

Ejemplo: Vue 3 con vue-router

La sesión

// src/composables/sesion.ts
import { onScopeDispose, shallowRef, type ShallowRef } from 'vue'
import { sessionView, type SessionView } from '@/lib/api'
import { api } from '@/lib/tai'

export function useSession(): ShallowRef<SessionView> {
  const vista = shallowRef(sessionView(api.session))
  const dejar = api.session.subscribe((estado) => { vista.value = sessionView(api.session, estado) })
  onScopeDispose(dejar)
  return vista
}
<script setup lang="ts">
const session = useSession()
</script>

<template>
  <button v-if="session.hasPermission(PERMISSIONS.GESTION_USUARIOS.CREATE)">Nuevo usuario</button>
</template>

Las pantallas

// src/router.ts
import { createRouter, createWebHistory } from 'vue-router'
import { access, resolveRoutes } from '@/lib/api/routes'
import { sessionView } from '@/lib/api'
import { api } from '@/lib/tai'
import { routes } from '@/routes'

const resueltas = resolveRoutes(routes)
export const router = createRouter({ history: createWebHistory(), routes: [/* las de vue-router */] })

router.beforeEach(async (to) => {
  if (api.session.state.status === 'unknown') await api.session.restore()
  const acceso = access(resueltas, to.path, sessionView(api.session))
  if (acceso.status === 'login') return acceso.redirectTo
  if (acceso.status === 'forbidden') return { name: 'not-found' }
  if (acceso.status === 'underConstruction') return { name: 'en-construccion' }
  return true
})

// Al cerrarse la sesión, la pantalla actual se vuelve a comprobar: lleva al login.
// `force`: sin él, vue-router no vuelve a navegar a la URL en la que ya está, y no pasa por el guard.
api.session.onSessionEnd(() => {
  const { path, query } = router.currentRoute.value
  void router.replace({ path, query, force: true })
})

El menú

import { computed } from 'vue'
import { navigation } from '@/lib/api/routes'

const session = useSession()
const items = computed(() => navigation(resueltas, session.value.hasPermission, session.value.isAuthenticated))

Sin framework

Lo mismo, a mano:

await api.session.restore()
api.session.subscribe((estado) => {
  const vista = sessionView(api.session, estado)
  pintarMenú(navigation(resueltas, vista.hasPermission, vista.isAuthenticated))
  const acceso = access(resueltas, location.pathname, vista)
  if (acceso.status === 'login') location.assign(acceso.redirectTo)
})

Un adaptador para todos

Si un framework se usa en más de un proyecto, lo que tiene sentido es que su adaptador se genere como los de React y Svelte —con "<framework>": true en el bloque frontend— en vez de copiarse de proyecto en proyecto. Es un adaptador más sobre el mismo núcleo, con su test gemelo: el que comprueba que decide lo mismo que los otros dos con las mismas declaraciones.