Edifiko
Edifiko DocsDocumentación del proyecto
  • Overview
Proyecto
  • README
  • CONTRIBUTING
  • WORKFLOW
  • Workflows CI/CD
  • UI Components
  • Showcase: Works UI Components
  • Schemas
  • Servicios
  • Hooks
  • Utils
  • Types
  • Constants
  • Errors
  • API Materiales
Herramientas de IA
  • Agentes
  • Skills
  1. Documentación
  2. Servicios

Servicios

services/README.md

Servicios de Firestore

La capa de servicios actúa como la única interfaz entre la lógica de la aplicación y Firebase Firestore. Ningún componente ni hook accede al adaptador directamente; toda operación de datos pasa obligatoriamente por estas funciones.

Descripción General

Cada servicio encapsula las operaciones CRUD y las suscripciones en tiempo real de un dominio específico. Los servicios dependen de dos capas:

  • @/adapters/firebase-client-adapter — Abstracción sobre el SDK de Firebase (Firestore, Auth, Storage).
  • @/schemas/ — Tipos TypeScript inferidos de Zod, que actúan como Single Source of Truth para la forma de cada entidad.

El patrón aplicado en todos los servicios es idéntico: se obtiene la instancia de Firestore con getFirestoreInstance(), se delega la operación al adaptador con la colección o ruta correcta, y se inyectan createdAt/updatedAt donde corresponde.

Estructura de Archivos

text
services/
├── categories/
│   ├── category-catalog.ts       # Composición: agrupa grupos + categorías + subcategorías en paralelo
│   ├── category-catalog.test.ts
│   ├── group-service.ts          # CRUD + suscripciones para grupos
│   ├── group-service.test.ts
│   ├── category-service.ts       # CRUD + suscripciones para categorías
│   ├── category-service.test.ts
│   ├── subcategory-service.ts    # CRUD + suscripciones para subcategorías
│   └── subcategory-service.test.ts
├── users/
│   ├── user-service.ts           # CRUD + suscripciones para usuarios
│   └── user-service.test.ts
├── materials/
│   ├── material-service.ts            # CRUD + suscripciones (SDK cliente, tiempo real)
│   ├── material-service.test.ts
│   ├── material-admin-service.ts      # Consultas servidor con Admin SDK (caché + filtros)
│   ├── material-admin-service.test.ts
│   └── material-categorization.ts     # Resuelve IGroup + ICategory + ISubcategory para un material
└── auth/                         # (pendiente de implementación)

Patrón Común

Todos los servicios siguen la misma estructura interna:

ts
// 1. Obtener instancia de Firestore
const db = getFirestoreInstance();

// 2. Delegar al adaptador con la colección (getCollection, createDocument…)
//    o con la ruta de documento (getDocument, updateDocument, deleteDocument)
return getCollection<IEntidad>(db, COLLECTIONS.ENTIDADES);

// 3. En create: inyectar timestamps
const now = new Date();
return createDocument<IEntidad>(db, COLLECTIONS.ENTIDADES, {
  ...data,
  createdAt: now,
  updatedAt: now,
});

// 4. En update: inyectar updatedAt
await updateDocument<IEntidad>(db, COLLECTION_PATHS.entidad(id), {
  ...data,
  updatedAt: new Date(),
});

Las rutas de documento se construyen con los builders de COLLECTION_PATHS (definidos en schemas/collections.ts). Los nombres de colección provienen de la constante COLLECTIONS (definida en constants/firebase.constants.ts).

� Composición de Catálogo

Archivo: services/categories/category-catalog.ts

Función de composición que obtiene grupos, categorías y subcategorías en paralelo con Promise.all y los agrupa en un array de ICategoryCatalog.

ts
export async function getCategoryCatalog(): Promise<Array<ICategoryCatalog>> {
  const [groups, categories, subcategories] = await Promise.all([
    getAllGroups(),
    getAllCategories(),
    getAllSubcategories(),
  ]);

  return groups.map((group) => {
    const groupCategories = categories.filter((c) => c.groupId === group.id);
    const categoryIds = new Set(groupCategories.map((c) => c.id));
    const groupSubcategories = subcategories.filter((s) => categoryIds.has(s.categoryId));
    return { group, categories: groupCategories, subcategories: groupSubcategories };
  });
}

El resultado se transforma luego a IGroupCatalogView[] mediante buildGroupCatalogViews() en utils/categories/build-catalog.ts antes de ser consumido por los componentes.

Testing: category-catalog.test.ts cubre 7 escenarios: resultado vacío, agrupación correcta, exclusión cruzada de grupos, categorías vacías, llamadas paralelas verificadas, propagación de errores y múltiples grupos.


🗂️ Servicio de Grupos

Colección Firestore: groups
Tipos: IGroup, ICreateGroup, IUpdateGroup — de @/schemas/categories/category.schema

Funciones CRUD

FunciónFirmaDescripción
getAllGroups() → Promise<IGroup[]>Retorna todos los documentos de la colección groups
getGroupById(id: string) → Promise<IGroup | null>Busca un grupo por su ID. Retorna null si no existe
createGroup(data: ICreateGroup) → Promise<IGroup>Crea un nuevo grupo con createdAt y updatedAt inyectados
updateGroup(id: string, data: IUpdateGroup) → Promise<void>Actualiza campos del grupo e inyecta updatedAt
deleteGroup(id: string) → Promise<void>Elimina el documento del grupo

Funciones de Suscripción

FunciónFirmaDescripción
subscribeToGroups(onNext, onError?) → UnsubscribeEscucha cambios en toda la colección groups
subscribeToGroup(id, onNext, onError?) → UnsubscribeEscucha cambios en un grupo específico

🗂️ Servicio de Categorías

Archivo: services/categories/category-service.ts
Colección Firestore: categories
Tipos: ICategory, ICreateCategory, IUpdateCategory — de @/schemas/categories/category.schema

Una categoría pertenece a un grupo a través del campo groupId.

Funciones CRUD

FunciónFirmaDescripción
getAllCategories() → Promise<ICategory[]>Retorna todos los documentos de la colección categories
getCategoryById(id: string) → Promise<ICategory | null>Busca una categoría por ID. Retorna null si no existe
createCategory(data: ICreateCategory) → Promise<ICategory>Crea una nueva categoría con timestamps inyectados
updateCategory(id: string, data: IUpdateCategory) → Promise<void>Actualiza campos e inyecta updatedAt
deleteCategory(id: string) → Promise<void>Elimina el documento de la categoría

Funciones de Suscripción

FunciónFirmaDescripción
subscribeToCategories(onNext, onError?) → UnsubscribeEscucha cambios en toda la colección categories
subscribeToCategory(id, onNext, onError?) → UnsubscribeEscucha cambios en una categoría específica

🗂️ Servicio de Subcategorías

Archivo: services/categories/subcategory-service.ts
Colección Firestore: subcategories
Tipos: ISubcategory, ICreateSubcategory, IUpdateSubcategory — de @/schemas/categories/category.schema

Una subcategoría pertenece a una categoría a través del campo categoryId.

Funciones CRUD

FunciónFirmaDescripción
getAllSubcategories() → Promise<ISubcategory[]>Retorna todos los documentos de la colección subcategories
getSubcategoryById(id: string) → Promise<ISubcategory | null>Busca una subcategoría por ID. Retorna null si no existe
createSubcategory(data: ICreateSubcategory) → Promise<ISubcategory>Crea una nueva subcategoría con timestamps inyectados
updateSubcategory(id: string, data: IUpdateSubcategory) → Promise<void>Actualiza campos e inyecta updatedAt
deleteSubcategory(id: string) → Promise<void>Elimina el documento de la subcategoría

Funciones de Suscripción

FunciónFirmaDescripción
subscribeToSubcategories(onNext, onError?) → UnsubscribeEscucha cambios en toda la colección subcategories
subscribeToSubcategory(id, onNext, onError?) → UnsubscribeEscucha cambios en una subcategoría específica

👤 Servicio de Usuarios

Archivo: services/users/user-service.ts
Colección Firestore: users
Tipos: IUser, ICreateUser, IUpdateUser — de @/schemas/users/user.schema

Funciones CRUD

FunciónFirmaDescripción
getAllUsers() → Promise<IUser[]>Retorna todos los documentos de la colección users
getUserById(id: string) → Promise<IUser | null>Busca un usuario por ID. Retorna null si no existe
createUser(data: ICreateUser) → Promise<IUser>Crea un nuevo usuario con timestamps inyectados
updateUser(id: string, data: IUpdateUser) → Promise<void>Actualiza campos del usuario e inyecta updatedAt
deleteUser(id: string) → Promise<void>Elimina el documento del usuario

Funciones de Suscripción

FunciónFirmaDescripción
subscribeToUsers(onNext, onError?) → UnsubscribeEscucha cambios en toda la colección users
subscribeToUser(id, onNext, onError?) → UnsubscribeEscucha cambios en un usuario específico

📦 Servicio de Materiales (Cliente)

Archivo: services/materials/material-service.ts
Colección Firestore: materials
Tipos: IMaterial, ICreateMaterial, IUpdateMaterial — de @/schemas/materials/material.schema

Este servicio usa el SDK cliente de Firebase y opera en el contexto del navegador. Además del CRUD estándar, expone createMaterialsBatch para importaciones masivas.

Un material almacena de forma desnormalizada los tres niveles de categorización: groupId, categoryId, subcategoryId y los nombres correspondientes (groupName, categoryName, subcategoryName).

Funciones CRUD

FunciónFirmaDescripción
getAllMaterials() → Promise<IMaterial[]>Retorna todos los documentos de la colección materials
getMaterialById(id: string) → Promise<IMaterial | null>Busca un material por ID. Retorna null si no existe
createMaterial(data: ICreateMaterial) → Promise<IMaterial>Crea un nuevo material con timestamps y todos los campos de categorización
updateMaterial(id, data: IUpdateMaterial) → Promise<void>Actualiza campos del material e inyecta updatedAt
deleteMaterial(id: string) → Promise<void>Elimina el documento del material
createMaterialsBatch(items: ICreateMaterial[]) → Promise<void>Crea múltiples materiales en lote usando writeBatch. Asigna id y path a cada documento. Límite de 499 por chunk.

Funciones de Suscripción

FunciónFirmaDescripción
subscribeToMaterials(onNext, onError?) → UnsubscribeEscucha cambios en toda la colección materials
subscribeToMaterial(id, onNext, onError?) → UnsubscribeEscucha cambios en un material específico

📦 Servicio de Materiales (Admin / Servidor)

Archivo: services/materials/material-admin-service.ts
Adaptador: @/adapters/firebase-admin-adapter
Uso: Exclusivo en Route Handlers de Next.js (contexto de servidor)

Este servicio usa el SDK Admin de Firebase, que opera con privilegios elevados y no está sujeto a las Firestore Security Rules del cliente. Expone dos funciones con comportamientos diferenciados según el rol del usuario.

Funciones

FunciónFirmaDescripción
getCachedMaterials() → Promise<IMaterial[]>Consulta cacheada con 'use cache' (Next.js ISR). Se invalida con cacheTag('materials'). Filtra por isActive === true.
getMaterials(filters?: IMaterialFilters) → Promise<IMaterial[]>Consulta en vivo, sin caché. Aplica filtros compuestos en Firestore. Usada para el rol admin.

Filtros disponibles (IMaterialFilters)

ts
interface IMaterialFilters {
  groupId?: string;
  categoryId?: string;
  subcategoryId?: string;
}

Los filtros se componen internamente con buildConstraints(): cada filtro presente agrega un where() sobre la query de Firestore. La presencia de múltiples filtros requiere índices compuestos (ver firestore.indexes.json).

Testing: material-admin-service.test.ts cubre 10 escenarios: constraint activo base, filtro por grupo/categoría/subcategoría/todos combinados, resultado vacío y propagación de errores.


🔄 Flujo de Datos

Componente / Hook │ │ llama a función del servicio ▼ Service (services/[dominio]/[entidad]-service.ts) │ │ getFirestoreInstance() + adaptador cliente ▼ Firebase Client Adapter (@/adapters/firebase-client-adapter) │ │ SDK de Firebase (firebase/firestore) ▼ Firestore (colección en GCP)

Para consultas de servidor (Next.js Route Handlers), el flujo usa el Admin SDK:

Route Handler (app/api/[ruta]/route.ts) │ │ verifyIdToken() + getMaterials() / getCachedMaterials() ▼ Service Admin (services/materials/material-admin-service.ts) │ │ getCollection() + AdminQueryConstraint[] ▼ Firebase Admin Adapter (@/adapters/firebase-admin-adapter) │ │ firebase-admin SDK (sin reglas de seguridad del cliente) ▼ Firestore (colección en GCP)

Para suscripciones en tiempo real, el flujo es inverso: Firestore emite un snapshot → el adaptador lo deserializa → el servicio lo propaga vía onNext → el hook actualiza el estado.

🔐 Seguridad y Roles

El control de acceso a las colecciones se define en las Firestore Security Rules del proyecto (firestore.rules), no en la capa de servicios. Los servicios asumen que el usuario autenticado tiene permisos sobre los recursos que solicita; si Firestore rechaza la operación, el error se propaga tal cual hacia arriba.

Nota: En el estado actual de desarrollo, firestore.rules permite read, write: if true para facilitar la iteración. Las reglas de producción por rol se definirán en una fase posterior.

La API REST de materiales (app/api/materials/route.ts) implementa su propio control de acceso a nivel de Route Handler: verifica el token Authorization: Bearer <token> con verifyIdToken() del Admin SDK y deriva a getMaterials() (sin caché, con filtros Firestore) para el rol admin, o a getCachedMaterials() para todos los demás.

ColecciónAcceso esperado
usersSolo admin puede listar todos; cada usuario puede leer/actualizar su propio documento
groups, categories, subcategoriesLectura pública para usuarios autenticados; escritura solo para admin
materialsLectura para todos los roles; escritura para admin y supplier_owner

🔁 Uso con Suscripciones en tiempo real

Las funciones subscribeTo* retornan una función Unsubscribe que debe invocarse al desmontar el componente o cuando la suscripción ya no sea necesaria, para evitar memory leaks.

ts
// Ejemplo de uso en un hook con useEffect
useEffect(() => {
  const unsubscribe = subscribeToGroups(
    (groups) => setGroups(groups),
    (error) => console.error(error),
  );

  return () => unsubscribe(); // limpieza al desmontar
}, []);

Cómo Agregar un Nuevo Servicio

  1. Crear el archivo en services/[dominio]/[entidad]-service.ts.
  2. Importar los adaptadores desde @/adapters/firebase-client-adapter.
  3. Importar COLLECTIONS desde @/constants/firebase.constants y COLLECTION_PATHS desde @/schemas/collections.
  4. Importar los tipos I[Entidad], ICreate[Entidad], IUpdate[Entidad] del schema correspondiente.
  5. Implementar las 5 funciones CRUD + 2 de suscripción siguiendo el patrón descrito.
  6. Crear el archivo de tests [entidad]-service.test.ts mockeando @/adapters/firebase-client-adapter.