Especificación Técnica: cfg_modulos¶
Esquema: core.identidad
Base de Datos: core
Servicio: svc-identidad
Propósito: Configuración de módulos por empresa y aplicación para la navegación del núcleo.
1. Justificación y Mejoras de Arquitectura¶
- Configuración por empresa y aplicación: un módulo puede activarse en unas empresas y no en otras.
- Unique (empresa_id, aplicacion_id, codigo) evita duplicados por contexto.
- Índices en empresa y aplicación aceleran el armado de la navegación.
- RLS por empresa actual aísla la configuración.
2. Definiciones de Implementación¶
Table core.identidad.cfg_modulos {
id uuid [pk, default: `gen_random_uuid()`]
empresa_id bigint [not null]
aplicacion_id uuid [not null, ref: > cat_aplicaciones.id]
codigo varchar(50) [not null]
nombre varchar(150) [not null]
descripcion text
icono varchar(50)
orden int [not null, default: `0`]
is_visible boolean [default: `true`]
// Auditoría General
is_activo boolean [not null, default: `true`, note: 'Estado lógico del registro']
created_at timestamptz [not null, default: `now()`, note: 'Fecha de creación']
updated_at timestamptz [not null, default: `now()`, note: 'Fecha de última actualización']
deleted_at timestamptz [note: 'Fecha de eliminación lógica']
created_by uuid [note: 'Usuario creador']
updated_by uuid [note: 'Usuario modificador']
Indexes {
(empresa_id, aplicacion_id, codigo) [unique, name: 'uq_cfg_modulos_empresa_aplicacion_codigo']
empresa_id [name: 'ix_cfg_modulos_empresa_id']
aplicacion_id [name: 'ix_cfg_modulos_aplicacion_id']
}
// RLS por empresa actual
}
CREATE TABLE IF NOT EXISTS core.identidad.cfg_modulos (
id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
empresa_id BIGINT NOT NULL,
aplicacion_id UUID NOT NULL,
codigo VARCHAR(50) NOT NULL,
nombre VARCHAR(150) NOT NULL,
descripcion TEXT,
icono VARCHAR(50),
orden INTEGER NOT NULL DEFAULT 0,
is_visible BOOLEAN DEFAULT true,
-- Auditoría General
is_activo BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ,
created_by UUID,
updated_by UUID,
CONSTRAINT uq_cfg_modulos_empresa_aplicacion_codigo UNIQUE (empresa_id, aplicacion_id, codigo),
CONSTRAINT fk_cfg_modulos_aplicacion FOREIGN KEY (aplicacion_id) REFERENCES core.identidad.cat_aplicaciones (id)
);
CREATE INDEX IF NOT EXISTS ix_cfg_modulos_empresa_id ON core.identidad.cfg_modulos (empresa_id);
CREATE INDEX IF NOT EXISTS ix_cfg_modulos_aplicacion_id ON core.identidad.cfg_modulos (aplicacion_id);
-- Row-Level Security
ALTER TABLE core.identidad.cfg_modulos ENABLE ROW LEVEL SECURITY;
CREATE POLICY aislamiento_empresa ON core.identidad.cfg_modulos
FOR ALL
USING (empresa_id = NULLIF(current_setting('app.current_empresa_id', true), '')::BIGINT);
-- Comentarios
COMMENT ON COLUMN core.identidad.cfg_modulos.empresa_id IS 'Empresa configurada';
COMMENT ON COLUMN core.identidad.cfg_modulos.aplicacion_id IS 'Aplicación propietaria del módulo';
COMMENT ON COLUMN core.identidad.cfg_modulos.codigo IS 'Código del módulo';
COMMENT ON COLUMN core.identidad.cfg_modulos.nombre IS 'Nombre del módulo';
COMMENT ON COLUMN core.identidad.cfg_modulos.descripcion IS 'Descripción del módulo';
COMMENT ON COLUMN core.identidad.cfg_modulos.icono IS 'Clase o ruta del ícono';
COMMENT ON COLUMN core.identidad.cfg_modulos.orden IS 'Orden de presentación';
COMMENT ON COLUMN core.identidad.cfg_modulos.is_visible IS 'Visible en la navegación';
from datetime import datetime, date
from typing import Optional
import uuid
from sqlalchemy import (
BigInteger,
Boolean,
CheckConstraint,
Date,
DateTime,
ForeignKey,
Identity,
Index,
Integer,
SmallInteger,
String,
Text,
UniqueConstraint,
text,
)
from sqlalchemy.dialects import postgresql
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class CfgModulo(Base):
__tablename__ = 'cfg_modulos'
__table_args__ = (
UniqueConstraint('empresa_id', 'aplicacion_id', 'codigo', name='uq_cfg_modulos_empresa_aplicacion_codigo'),
Index('ix_cfg_modulos_empresa_id', 'empresa_id'),
Index('ix_cfg_modulos_aplicacion_id', 'aplicacion_id'),
{"schema": 'core.identidad'},
)
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
primary_key=True,
server_default=text("gen_random_uuid()"),
)
empresa_id: Mapped[int] = mapped_column(
BigInteger,
nullable=False,
comment='Empresa configurada',
)
aplicacion_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
nullable=False,
ForeignKey("cat_aplicaciones.id"),
comment='Aplicación propietaria del módulo',
)
codigo: Mapped[str] = mapped_column(
String(50),
nullable=False,
comment='Código del módulo',
)
nombre: Mapped[str] = mapped_column(
String(150),
nullable=False,
comment='Nombre del módulo',
)
descripcion: Mapped[str | None] = mapped_column(
Text,
comment='Descripción del módulo',
)
icono: Mapped[str | None] = mapped_column(
String(50),
comment='Clase o ruta del ícono',
)
orden: Mapped[int] = mapped_column(
Integer,
nullable=False,
server_default=text("0"),
comment='Orden de presentación',
)
is_visible: Mapped[bool | None] = mapped_column(
Boolean,
server_default=text("true"),
comment='Visible en la navegación',
)
# Auditoría General
is_activo: Mapped[bool] = mapped_column(
Boolean,
nullable=False,
server_default=text("true"),
comment='Estado lógico del registro',
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default=text("now()"),
comment='Fecha de creación',
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default=text("now()"),
comment='Fecha de última actualización',
)
deleted_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True),
comment='Fecha de eliminación lógica',
)
created_by: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
comment='Usuario creador',
)
updated_by: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
comment='Usuario modificador',
)
"""revision: core_id_0013
create table core.identidad.cfg_modulos
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision: str = 'core_id_0013'
down_revision: str | None = 'core_id_0012'
branch_labels: str | None = None
depends_on: str | None = None
def upgrade() -> None:
op.create_table(
'cfg_modulos',
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("empresa_id", sa.BigInteger(), nullable=False),
sa.Column("aplicacion_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("codigo", sa.String(length=50), nullable=False),
sa.Column("nombre", sa.String(length=150), nullable=False),
sa.Column("descripcion", sa.Text()),
sa.Column("icono", sa.String(length=50)),
sa.Column("orden", sa.Integer(), nullable=False, server_default=sa.text("0")),
sa.Column("is_visible", sa.Boolean(), server_default=sa.text("true")),
sa.Column("is_activo", sa.Boolean(), nullable=False, server_default=sa.text("true")),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
sa.Column("deleted_at", sa.DateTime(timezone=True)),
sa.Column("created_by", postgresql.UUID(as_uuid=True)),
sa.Column("updated_by", postgresql.UUID(as_uuid=True)),
sa.UniqueConstraint('empresa_id', 'aplicacion_id', 'codigo', name='uq_cfg_modulos_empresa_aplicacion_codigo'),
sa.ForeignKeyConstraint(['aplicacion_id'], ['core.identidad.cat_aplicaciones.id'], name='fk_cfg_modulos_aplicacion'),
schema='core.identidad',
)
op.create_index('ix_cfg_modulos_empresa_id', 'cfg_modulos', ['empresa_id'], unique=False, schema='core.identidad')
op.create_index('ix_cfg_modulos_aplicacion_id', 'cfg_modulos', ['aplicacion_id'], unique=False, schema='core.identidad')
op.execute(
"""
ALTER TABLE core.identidad.cfg_modulos ENABLE ROW LEVEL SECURITY;
CREATE POLICY aislamiento_empresa
ON core.identidad.cfg_modulos
FOR ALL
USING (empresa_id = NULLIF(current_setting('app.current_empresa_id', true), '')::BIGINT);
"""
)
def downgrade() -> None:
op.drop_table('cfg_modulos', schema='core.identidad')