Utils

packages/utils/README.md

@repo/utils

Utilidades agnósticas y reutilizables del monorepo. Actualmente expone helpers de permisos para resolver los roles y permisos de un usuario dentro de una comunidad.

Uso del package

El package se consume mediante su export público:

ts
import { hasPermission, getPermissionsByCommunity } from '@repo/utils';

Las utilidades de browser para localStorage también se consumen desde el export público:

ts
import { getLocalStorageItem, LOCAL_STORAGE_DATA_TYPES, setLocalStorageItem } from '@repo/utils';

setLocalStorageItem('communityId', 'community-1', LOCAL_STORAGE_DATA_TYPES.STRING);
const communityId = getLocalStorageItem<string>('communityId', LOCAL_STORAGE_DATA_TYPES.STRING);

Estas funciones son seguras para SSR y devuelven null o false cuando el storage no está disponible, está bloqueado o contiene datos inválidos.

No se deben importar archivos internos desde una app. Para agregar un módulo, crear una carpeta bajo src/, exportar su API desde el índice del módulo y después desde src/index.ts.

Comandos disponibles:

  • pnpm --filter @repo/utils test:run: ejecuta los tests.
  • pnpm --filter @repo/utils check-types: valida TypeScript sin emitir archivos.
  • pnpm --filter @repo/utils lint: ejecuta ESLint.
  • pnpm --filter @repo/utils build: compila el package.

Tests obligatorios

Cada nuevo util debe tener tests junto al módulo (*.test.ts). Cubrir como mínimo:

  • Caso exitoso.
  • Comunidad inexistente o mapa null/undefined.
  • Permiso o rol desconocido.
  • Para helpers booleanos, combinaciones positivas y negativas.
  • Para asserts, el error y su mensaje/código.

Utilidades de permisos

Los roles se resuelven exclusivamente con ROLE_PERMISSIONS de @repo/constants.

ts
type RolesMap = Record<string, Array<IUserRolesList>>;
getUserRolesInCommunity(rolesMap, communityId);
getPermissionsByCommunity(rolesMap, communityId);
hasPermission(permission, communityId, rolesMap);
hasAnyPermission(permissions, communityId, rolesMap);
hasAllPermissions(permissions, communityId, rolesMap);
assertCallerPermission(permission, communityId, rolesMap);
assertUserPermission(permission, communityId, rolesMap);

hasPermission valida una autorización dentro de una comunidad concreta; nunca debe evaluarse solo contra el rol global del usuario. hasAnyPermission sirve para acciones alternativas y hasAllPermissions para acciones que requieren todos los permisos. Los helpers assert* lanzan PERMISSION_DENIED y son útiles en servicios internos.

Estándar para Cloud Functions onCall

Las funciones callable que requieren autorización deben seguir este orden:

  1. Validar request.auth si la acción es privada.
  2. Parsear request.data con el schema de @repo/schemas.
  3. Extraer el communityId del payload validado.
  4. Declarar el permiso o permisos necesarios de la acción.
  5. Obtener request.auth.token.communities y verificar con hasPermission.
  6. Ejecutar el servicio de dominio.
  7. Devolver el response schema del dominio y mapear errores de negocio.

Para reutilizar los primeros cinco pasos usar authorizedOnCall desde @/shared/on-call/authorized-on-call:

ts
export const inviteUserOnCall = authorizedOnCall({
  options: { secrets: SMTP_ARRAY },
  schema: inviteUserOnCallInputSchema,
  requiredPermissions: ROLES_PERMISSIONS.USERS.USERS_DASHBOARD.EDIT_USERS_INVITATION.key,
  getCommunityId: (input) => input.communityId,
  errors: {
    unauthenticated: () => errorResponse(USERS_ERROR_CODES.PERMISSION_DENIED),
    invalidArgument: () => errorResponse(USERS_ERROR_CODES.INVALID_ARGUMENT),
    permissionDenied: () => errorResponse(USERS_ERROR_CODES.PERMISSION_DENIED),
  },
  handler: async (input, request) => service(input, request.auth.uid),
});

El wrapper acepta un permiso o una lista de permisos; una lista usa semántica any. Para dominios distintos se deben proporcionar sus propios constructores errors y response schema. La lógica de negocio, logging y mapeo de errores internos permanece en handler.

authorizeOnCallRequest también está exportada desde el módulo para probar de forma aislada autenticación, payload y permisos sin levantar Firebase. Cada nuevo wrapper especializado puede componerse sobre esta misma función, manteniendo los contratos de error del dominio.