erp-core-next
v0.2.0
Published
Núcleo standalone, sin NgModules y basado en signals para apps Angular: stores reactivos, capa HTTP funcional, guards y dos modos de arranque (gestor/consumer).
Maintainers
Readme
erp-core-next
Núcleo standalone, sin NgModules y basado en signals para apps Angular 20+ del ERP Visorus.
Reemplaza a la lib legacy erp-core (Angular 7→17) con stores reactivos, una capa HTTP
funcional y dos modos de arranque: gestor (provideErpCore) y consumer
(provideErpCoreConsumer), pensados para coexistir sin big-bang (patrón strangler fig).
Documentación completa (arquitectura, modelos, stores, HTTP, providers, migración) en
docs/erp-core-next/.
Instalación
Construye la lib y empaquétala:
npm run pack:lib # → dist/erp-core-next/erp-core-next-<version>.tgzEn el host instálala desde el tarball (o desde tu registry):
npm install [email protected] # o desde el tarball: ../erp-core-next/dist/erp-core-next/erp-core-next-0.2.0.tgzPeer dependencies: @angular/common, @angular/core y @angular/router ^20.0.0 || ^21.0.0, más rxjs ^7.8.
Uso básico
Modo GESTOR — provideErpCore(config, options?)
Para el host principal. Recibe la configuración del backend y, opcionalmente, opciones
(p. ej. coexistMode para convivir con la lib vieja registrando solo authInterceptor).
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import {
provideErpCore,
NOTIFICATION_SERVICE,
LOGIN_PROMPT_PORT,
type NotificationPort,
} from 'erp-core-next';
const notifications: NotificationPort = {
success: (m, t) => console.log('[notify] success', t ?? '', m),
info: (m, t) => console.log('[notify] info', t ?? '', m),
warning: (m, t) => console.log('[notify] warning', t ?? '', m),
error: (p, t) => console.log('[notify] error', t ?? '', p),
};
export const appConfig: ApplicationConfig = {
providers: [
// coexistMode: true → sólo authInterceptor (loading/error/response siguen siendo del host viejo)
provideErpCore({ apiUrl: 'https://api.midominio.com' }, { coexistMode: true }),
{ provide: NOTIFICATION_SERVICE, useValue: notifications },
{ provide: LOGIN_PROMPT_PORT, useValue: { prompt: () => console.log('[login] prompt') } },
],
};AppConfig: { apiUrl: string; assetsUrl?: string; storageKey?: string; host?: string }.
ErpCoreOptions: { extraInterceptors?; enableSuccessNotifications?; storage?; coexistMode? }.
Modo CONSUMER — provideErpCoreConsumer(options?)
Para subproyectos del ERP que consumen la sesión que dejó el lanzador (no gestionan
login/empresa/módulo). Lee assets/config.json al arranque y localStorage('usuario')
(vía LegacyTokenStorage, solo lectura: el lanzador es dueño de la sesión).
import { provideErpCoreConsumer, NOTIFICATION_SERVICE, LOGIN_PROMPT_PORT } from 'erp-core-next';
export const appConfig: ApplicationConfig = {
providers: [
provideErpCoreConsumer({
coexistMode: true, // solo authInterceptor (lib vieja viva)
enableSuccessNotifications: true, // toast «Exito» por body.success
configSource: 'launcher-first', // localStorage('config') primero (L4)
hydrateProfile: true, // loadProfile() al arranque si hay token
}),
{ provide: NOTIFICATION_SERVICE, useValue: notifications },
{ provide: LOGIN_PROMPT_PORT, useValue: { prompt: () => console.log('[login] prompt') } },
],
};ErpCoreConsumerOptions: { extraInterceptors?; coexistMode?; enableSuccessNotifications?;
hydrateProfile?; configSource?: 'assets-first' | 'launcher-first' } (default:
cadena completa, sin toasts de éxito, hidrata perfil, assets-first).
Desde 0.2.0, en modo consumer el token no queda congelado al arrancar (L1):
authorizationHeader() relee localStorage('usuario') en cada petición — un relogin
del lanzador en otra pestaña o un 401 transitorio no dejan a los servicios migrados
sin header. El ciclo de carga del perfil se publica en AuthStore.profileStatus
('idle' | 'loading' | 'loaded' | 'error', L3) para que el host replique la política
de «si falla el perfil → volver al lanzador».
import { provideErpCoreConsumer, NOTIFICATION_SERVICE, LOGIN_PROMPT_PORT } from 'erp-core-next';
export const appConfig: ApplicationConfig = {
providers: [
provideErpCoreConsumer(),
{ provide: NOTIFICATION_SERVICE, useValue: notifications },
{ provide: LOGIN_PROMPT_PORT, useValue: { prompt: () => console.log('[login] prompt') } },
],
};public/assets/config.json (servido en /assets/config.json):
{ "url": "https://erpb.visorus.com/api", "host": "https://erp.visorus.com" }CRUD en el host — BaseApiService<T>
El host crea sus servicios extendiendo BaseApiService<T>:
import { Injectable } from '@angular/core';
import { BaseApiService, type Empresa } from 'erp-core-next';
@Injectable({ providedIn: 'root' })
export class EmpresasService extends BaseApiService<Empresa> {
protected readonly resource = 'empresas'; // → GET {apiUrl}/empresas
}
// métodos: getAll, getPage, get, create, update, removeAPI pública (exports)
| Grupo | Símbolos |
|---|---|
| Providers | provideErpCore, provideErpCoreConsumer, ErpCoreOptions, ErpCoreConsumerOptions · testing: provideErpCoreTesting (entry secundario erp-core-next/testing) |
| Stores | ConfigStore, AuthStore (incl. profileStatus, ProfileStatus), LoadingStore, AccessControlStore |
| HTTP | BaseApiService, authInterceptor, loadingInterceptor, responseInterceptor, errorInterceptor, SKIP_AUTH, SUPPRESS_LOADING, normalizeError |
| Guards | authGuard, consumerAuthGuard (sin sesión → redirectToHost), roleGuard |
| Tokens / Puertos | APP_CONFIG, CLOCK, NOTIFICATION_SERVICE (NotificationPort), LOGIN_PROMPT_PORT (LoginPromptPort), ERP_CONSUMER_MODE |
| Storage | TokenStorage (abstracta), LocalStorageTokenStorage, InMemoryTokenStorage, LegacyTokenStorage |
| Consumer utils | loadAppConfig, readLauncherConfig, setResolvedConfig, appConfigFactory, redirectToHost, AppConfig |
| Auth models | AuthToken, AuthTokenDto, toAuthToken, isTokenExpired, toAuthorizationHeader, LoginRequest, User, UserDetails, Role, Authority, Empresa, ModuloAcceso, LegacyUsuarioDto, normalizeLegacyRoles |
| Common models | ID, Page, PageRequest, ApiResponse, ApiError, NormalizedError, HttpErrorCode, isApiError |
| Domain models | PersonaFisica, PersonaMoral, DatoContacto, DatoDireccion, createPersonaMoral |
Coexistencia con la lib vieja (coexistMode)
Durante la migración, coexistMode: true hace que el core registre solo authInterceptor
(loading/error/response siguen siendo del host viejo), evitando dobles spinners y toasts.
El authInterceptor respeta una cabecera Authorization preexistente (no la duplica), lo que
permite que CrudService viejo y servicios nuevos convivan. Ver
MIGRATION-CHECKLIST.md Fase 1.
Supuestos del backend aún sin confirmar
De HTTP-ADVANCED.md §"Supuestos". Mientras no se
confirmen, estas features quedan desactivadas (enableRefresh: false, retryPolicy: undefined).
[CONFIRMAR]Endpoint de refresh: se asumePOST {apiUrl}/token/refreshcon body{ refresh_token }, retornando unAuthTokenDto(mismo contrato que/login). AjustarrefreshEndpoint.[CONFIRMAR]Rotación del refresh token: se asume que cada refresh devuelve un nuevorefresh_token. Si no rota, el refresh sigue funcionando (reutiliza el mismo).[CONFIRMAR]Semántica del 401: se asume que 401 = "access token expirado/inválido y refrescable". Si el backend distingue "refresh token inválido" con un código/sub-código, añadir ese check para ir directo a logout.[CONFIRMAR]Idempotencia: se asume GET/HEAD/OPTIONS como seguros para retry. Ajustar si el backend tiene endpoints POST idempotentes explícitos.
Desarrollo
ng build erp-core-next # verde obligatorio tras cada cambio
ng test erp-core-next # Vitest (@angular/build:unit-test)