Schemas
schemas/README.md
schemas/
Fuente única de verdad (SSOT) para la validación de datos, tipado y contratos de formularios de Edifiko. Todos los esquemas están construidos con Zod y organizados por dominio de negocio.
Arquitectura y convenciones
| Regla | Detalle |
|---|---|
| Zod como SSOT | Toda entidad de BD o payload de formulario nace de un esquema Zod. Los tipos de TypeScript se infieren con z.infer<typeof schema>. |
camelCase para const | Las variables de esquema usan camelCase (userSchema, categoryRecordSchema). Enforceado por ESLint @typescript-eslint/naming-convention. |
PascalCase para type | Los tipos inferidos usan PascalCase (User, CategoryRecord). |
| Tests obligatorios | Cada schema tiene su archivo .test.ts adyacente al schema. |
| Mensajes en español | Los mensajes de error de Zod visibles al usuario deben estar en español. |
| Un folder por dominio | Cada colección de Firestore tiene su propia carpeta dentro de schemas/. |
collections.ts
Archivo raíz con las utilidades compartidas por todos los módulos.
COLLECTION_PATHS
Builders de rutas de documentos Firestore, tipados con los nombres de colección de constants/firebase.constants.ts.
import { COLLECTION_PATHS } from '@/schemas/collections';
COLLECTION_PATHS.user('uid-123'); // → 'users/uid-123'
COLLECTION_PATHS.category('cat-456'); // → 'categories/cat-456'
COLLECTION_PATHS.material('mat-789'); // → 'materials/mat-789'
COLLECTION_PATHS.supplierCatalogItem('x'); // → 'supplier_catalog/x'
COLLECTION_PATHS.project('proj-1'); // → 'projects/proj-1'
COLLECTION_PATHS.projectMaterial('pm-1'); // → 'project_materials/pm-1'
COLLECTION_PATHS.quote('q-1'); // → 'quotes/q-1'
COLLECTION_PATHS.company('co-1'); // → 'companies/co-1'
baseDocumentSchema
Campos generados por Firestore, siempre presentes en documentos leídos de la BD. Se extiende con .extend() para crear los RecordSchema de cada módulo.
{
id: z.string().min(1); // ID del documento
path: z.string().min(1); // Ruta completa del documento
createdAt: z.date();
updatedAt: z.date();
}
Módulos implementados
users/
Colección Firestore: /users/{userId}
| Export | Descripción |
|---|---|
USER_ROLES | Array as const con los 6 roles del sistema |
userRoleSchema | z.enum(USER_ROLES) |
baseUserSchema | Campos de escritura: email, name, role, companyId |
userSchema | baseUserSchema + baseDocumentSchema — registro completo de BD |
createUserSchema | Alias de baseUserSchema (sin campos de BD) |
updateUserSchema | userSchema.partial() — todos los campos opcionales |
Roles disponibles: admin · architect_owner · architect_employee · supplier_owner · supplier_sales · supplier_logistics
categories/
Colección Firestore: /categories/{categoryId}
Este módulo gestiona la jerarquía completa de 3 niveles: Rubro → Categoría → Subcategoría.
Rubro (IGroup)
| Export | Descripción |
|---|---|
baseGroupSchema | title (2–50), description? (máx 120), iconName, isActive |
groupSchema | baseGroupSchema + baseDocumentSchema |
createGroupSchema | Alias de baseGroupSchema |
updateGroupSchema | groupSchema.partial() |
Categoría (ICategory)
| Export | Descripción |
|---|---|
baseCategorySchema | groupId, name (2–80), isActive |
categorySchema | baseCategorySchema + baseDocumentSchema |
createCategorySchema | Alias de baseCategorySchema |
updateCategorySchema | categorySchema.partial() |
Subcategoría (ISubcategory)
| Export | Descripción |
|---|---|
baseSubcategorySchema | categoryId, name (2–80), isActive |
subcategorySchema | baseSubcategorySchema + baseDocumentSchema |
createSubcategorySchema | Alias de baseSubcategorySchema |
updateSubcategorySchema | subcategorySchema.partial() |
materials/
Colección Firestore: /materials/{materialId}
| Export | Descripción |
|---|---|
MATERIAL_UNITS | Array as const con las unidades válidas del sistema |
materialUnitSchema | z.enum(MATERIAL_UNITS) |
baseMaterialSchema | Campos de escritura: name, subcategoryId, categoryId, groupId, groupName?, categoryName?, subcategoryName?, isActive |
materialSchema | baseMaterialSchema + baseDocumentSchema — registro completo de BD |
createMaterialSchema | Alias de baseMaterialSchema |
updateMaterialSchema | baseMaterialSchema.partial() |
Campos de categorización desnormalizados:
Además del subcategoryId (FK), el schema almacena los nombres de los tres niveles de la jerarquía para evitar joins en lectura:
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
groupId | string | ✅ | ID del rubro al que pertenece |
categoryId | string | ✅ | ID de la categoría al que pertenece |
subcategoryId | string | ✅ | ID de la subcategoría |
groupName | string? | ❌ | Nombre del rubro (desnormalizado) |
categoryName | string? | ❌ | Nombre de la categoría (desnorm.) |
subcategoryName | string? | ❌ | Nombre de la subcategoría (desnorm.) |
Unidades disponibles: unidad · bolsa · kg · tonelada · litro · metro · m2 · m3
Módulos pendientes (carpetas vacías)
| Carpeta | Colección Firestore | Estado |
|---|---|---|
companies/ | /companies/{companyId} | Pendiente |
products/ | /supplier_catalog/{itemId} | Pendiente |
projects/ | /projects/{projectId} | Pendiente |
lists/ | /project_materials/{itemId} | Pendiente |
quotes/ | /quotes/{quoteId} | Pendiente |
Cómo agregar un nuevo schema
- Crear la carpeta
schemas/{dominio}/. - Crear
{dominio}.schema.tssiguiendo el patrón:
import { z } from 'zod';
import { baseDocumentSchema } from '@/schemas/collections';
export const baseFooSchema = z.object({
// campos de escritura (CREATE / UPDATE)
});
export type IFoo = z.infer<typeof baseFooSchema>;
export const fooSchema = z.object({
...baseFooSchema.shape,
...baseDocumentSchema.shape,
});
export type IFooRecord = z.infer<typeof fooSchema>;
export const createFooSchema = baseFooSchema;
export type ICreateFoo = z.infer<typeof createFooSchema>;
export const updateFooSchema = baseFooSchema.partial();
export type IUpdateFoo = z.infer<typeof updateFooSchema>;
- Crear
{dominio}.schema.test.tscon casos de éxito y fallo para cada campo. - Ejecutar
pnpm testypnpm lintpara verificar.
