Modelo de Dados
Versão: 1.0
< Backward:
- RF-001 a RF-024;
- RNF-001 a RNF-003;
- G-000 a G-014.7 (KAOS);
- ASR-001 a ASR-015.
Modelo Entidade-Relacionamento (MER) — Diagrama ER Lógico
erDiagram
Usuario ||--o{ Caderno : "possui"
Usuario ||--o{ Sessao : "possui"
Usuario ||--o{ Item : "possui (proprietário direto)"
Caderno ||--o{ Item : "contém (quando classificado)"
Item ||--o{ SubItem : "possui (apenas type=TODO)"
Item |o--o| Item : "TODO associa-se a um SCHEDULE (scheduleId)"
Usuario {
uuid id PK
string name
string email UK
string passwordHash
string status "ATIVA | BLOQUEADA_TEMPORARIAMENTE | DESATIVADA | EXCLUIDA"
int failedLoginAttempts "default 0"
datetime lockedUntil "nullable"
datetime createdAt
datetime updatedAt
}
Sessao {
uuid id PK
uuid userId FK
string refreshTokenHash
string userAgent "nullable"
datetime createdAt
datetime revokedAt "nullable"
datetime expiresAt
}
Caderno {
uuid id PK
uuid userId FK
string name
datetime createdAt
datetime updatedAt
}
Item {
uuid id PK
uuid userId FK "proprietário direto - ver ADR-023"
uuid notebookId FK "nullable, null = Inbox - ver ADR-020"
string type "SCHEDULE | TODO (discriminador STI - ADR-004)"
string title
int priority "nullable, 1 a 4"
string status "ATIVO | ARQUIVADO - ver ADR-024"
boolean isCompleted "apenas TODO - ver ADR-024"
datetime startAt "obrigatório se type=SCHEDULE"
datetime endAt "obrigatório se type=SCHEDULE"
datetime dueDate "nullable, apenas TODO"
string recurrenceFrequency "nullable: DAILY|WEEKLY|MONTHLY"
datetime recurrenceEndDate "nullable"
datetime snoozedUntil "nullable - ver ADR-014 (Arquitetura)"
uuid scheduleId FK "nullable, auto-relacionamento TODO->SCHEDULE - ver ADR-022"
datetime createdAt
datetime updatedAt
}
SubItem {
uuid id PK
uuid itemId FK "obrigatório, aponta para Item type=TODO"
string title
boolean isCompleted "default false"
datetime createdAt
datetime updatedAt
}
Relacionamentos
| Relacionamento |
Cardinalidade |
Descrição |
| Usuario — Caderno |
1:N |
Um usuário possui zero ou mais cadernos |
| Usuario — Sessao |
1:N |
Um usuário possui zero ou mais sessões |
| Usuario — Item |
1:N |
Um usuário é proprietário direto de zero ou mais itens |
| Caderno — Item |
1:N (opcional) |
Um caderno contém zero ou mais itens; um item pode não pertencer a nenhum caderno (Inbox) |
| Item — SubItem |
1:N |
Um item (type=TODO) possui zero ou mais sub-itens |
| Item — Item |
0..1:0..1 |
Um TODO pode associar-se opcionalmente a um SCHEDULE via scheduleId |
Dicionário de Dados
Entidade: Usuario
| Atributo |
Tipo |
Restrições |
Descrição |
| id |
UUID |
PK, NOT NULL |
Identificador único do usuário |
| name |
VARCHAR |
NOT NULL |
Nome do usuário |
| email |
VARCHAR |
PK alternativa (UK), NOT NULL, UNIQUE |
E-mail do usuário (usado como login) |
| passwordHash |
VARCHAR |
NOT NULL |
Hash da senha (Argon2id) — nunca armazenada em texto puro |
| status |
ENUM |
NOT NULL |
Estado da conta: ATIVA, BLOQUEADA_TEMPORARIAMENTE, DESATIVADA, EXCLUIDA |
| failedLoginAttempts |
INTEGER |
NOT NULL, DEFAULT 0 |
Contador de tentativas de login falhas consecutivas |
| lockedUntil |
TIMESTAMP |
NULLABLE |
Data/hora até a qual a conta permanece bloqueada |
| createdAt |
TIMESTAMP |
NOT NULL |
Data/hora de criação do registro |
| updatedAt |
TIMESTAMP |
NOT NULL |
Data/hora da última atualização |
Entidade: Sessao
| Atributo |
Tipo |
Restrições |
Descrição |
| id |
UUID |
PK, NOT NULL |
Identificador único da sessão |
| userId |
UUID |
FK → Usuario.id, NOT NULL, ON DELETE CASCADE |
Usuário proprietário da sessão |
| refreshTokenHash |
VARCHAR |
NOT NULL, INDEXED |
Hash do refresh token opaco |
| userAgent |
VARCHAR |
NULLABLE |
Identificação do dispositivo/navegador |
| createdAt |
TIMESTAMP |
NOT NULL |
Data/hora de criação da sessão |
| revokedAt |
TIMESTAMP |
NULLABLE |
Data/hora da revogação (NULL = sessão ativa) |
| expiresAt |
TIMESTAMP |
NOT NULL |
Data/hora de expiração natural do refresh token |
Entidade: Caderno
| Atributo |
Tipo |
Restrições |
Descrição |
| id |
UUID |
PK, NOT NULL |
Identificador único do caderno |
| userId |
UUID |
FK → Usuario.id, NOT NULL, ON DELETE CASCADE |
Usuário proprietário do caderno |
| name |
VARCHAR |
NOT NULL |
Nome do caderno |
| createdAt |
TIMESTAMP |
NOT NULL |
Data/hora de criação |
| updatedAt |
TIMESTAMP |
NOT NULL |
Data/hora da última atualização |
Entidade: Item
| Atributo |
Tipo |
Restrições |
Descrição |
| id |
UUID |
PK, NOT NULL |
Identificador único do item |
| userId |
UUID |
FK → Usuario.id, NOT NULL, ON DELETE CASCADE |
Proprietário direto (ADR-023) |
| notebookId |
UUID |
FK → Caderno.id, NULLABLE, ON DELETE SET NULL |
Caderno ao qual pertence; NULL = Inbox (ADR-020) |
| type |
ENUM |
NOT NULL |
Discriminador STI: SCHEDULE ou TODO (ADR-004) |
| title |
VARCHAR |
NOT NULL, INDEXED (GIN pg_trgm) |
Título do item |
| priority |
INTEGER |
NULLABLE, CHECK (1–4) |
Prioridade: 1 (Crítica) a 4 (Baixa); NULL = sem prioridade |
| status |
ENUM |
NOT NULL |
Estado no ciclo de vida: ATIVO ou ARQUIVADO (ADR-024) |
| isCompleted |
BOOLEAN |
NOT NULL, DEFAULT false |
Flag de conclusão — apenas para type=TODO (ADR-024) |
| startAt |
TIMESTAMP |
NULLABLE (obrigatório se SCHEDULE) |
Data/hora de início — obrigatório para SCHEDULE |
| endAt |
TIMESTAMP |
NULLABLE (obrigatório se SCHEDULE) |
Data/hora de término — obrigatório para SCHEDULE |
| dueDate |
TIMESTAMP |
NULLABLE |
Data limite — apenas para TODO |
| recurrenceFrequency |
ENUM |
NULLABLE |
Frequência de recorrência: DAILY, WEEKLY, MONTHLY |
| recurrenceEndDate |
TIMESTAMP |
NULLABLE |
Data de término da série recorrente |
| snoozedUntil |
TIMESTAMP |
NULLABLE |
Data/hora até a qual o item fica oculto (Snooze) |
| scheduleId |
UUID |
FK → Item.id (self), NULLABLE, ON DELETE SET NULL |
Auto-relacionamento TODO→SCHEDULE (ADR-022) |
| createdAt |
TIMESTAMP |
NOT NULL |
Data/hora de criação |
| updatedAt |
TIMESTAMP |
NOT NULL |
Data/hora da última atualização |
Entidade: SubItem
| Atributo |
Tipo |
Restrições |
Descrição |
| id |
UUID |
PK, NOT NULL |
Identificador único do sub-item |
| itemId |
UUID |
FK → Item.id, NOT NULL, ON DELETE CASCADE |
Item pai (obrigatoriamente type=TODO) |
| title |
VARCHAR |
NOT NULL |
Título/descrição do sub-item |
| isCompleted |
BOOLEAN |
NOT NULL, DEFAULT false |
Flag de conclusão do sub-item |
| createdAt |
TIMESTAMP |
NOT NULL |
Data/hora de criação |
| updatedAt |
TIMESTAMP |
NOT NULL |
Data/hora da última atualização |
Rastreabilidade Backward por entidade
| Entidade |
Backward |
Usuario |
G-014, RF-016 a RF-024, RNF-003 |
Sessao |
G-014.3, RF-021, ASR-003, ADR-006 |
Caderno |
G-000, G-001, G-001.1, RF-001, RF-002 |
Item |
G-001.2, G-003 a G-012, RF-003, RF-005 a RF-011, RF-015, ASR-009, ASR-010 |
SubItem |
G-001.3, G-008.2, RF-003, RF-011 |
Decisões de Modelagem
- ADR-020: Estado “Inbox” representado implicitamente por
notebookId nulo, sem enum dedicado.
- ADR-021: Deleção de Item como exclusão física (hard delete), sem estado “Deletado” persistido.
- ADR-022: Auto-relacionamento unidirecional no Item (TODO → SCHEDULE) via
scheduleId.
- ADR-023: Proprietário direto (
userId) no Item, independente do Caderno.
- ADR-024: “Concluído” modelado como flag booleano (
isCompleted), não como valor do enum status.
Notas de Implementação
Índices
- Usuario.email — índice único (UK), usado em Login e Cadastro (RF-017, RF-018).
- Item.userId, Item.notebookId, Item.status — índices simples para as queries transversais de Todo List, Planner e Calendário (ASR-013, ASR-014).
- Item.title — índice GIN trigram (pg_trgm), restrito por WHERE type = 'TODO' na query de Busca (ADR-016).
- Sessao.refreshTokenHash — índice para lookup rápido na renovação de Access Token (ADR-006).
Constraints e validações
- Item.priority — CHECK (priority IS NULL OR priority BETWEEN 1 AND 4).
- Obrigatoriedade condicional por tipo (startAt/endAt obrigatórios se type = SCHEDULE; dueDate/isCompleted só fazem sentido se type = TODO) não é imposta pelo schema — é responsabilidade da camada de domínio (Item rico, ADR-012), conforme trade-off já assumido em ADR-004.
- SubItem.itemId deve referenciar exclusivamente um Item com type = TODO — validado no domínio (ASR-009), sem constraint declarativa no banco (Prisma/SQL não expressam “FK condicional por valor de outra coluna”).
- Item.scheduleId deve referenciar exclusivamente um Item com type = SCHEDULE, e só pode ser preenchido em itens type = TODO — validado no domínio (ADR-022).
Cascata e integridade referencial
- Usuario → Caderno, Usuario → Item, Usuario → Sessao: ON DELETE CASCADE (exclusão de conta, RF-023, ADR-010).
- Caderno → Item: sem cascade automático — exclusão de Caderno seta notebookId = NULL explicitamente via transação de aplicação (RF-011, ADR-010), preservando o item na Inbox.
- Item → SubItem: ON DELETE CASCADE (deleção de TODO remove seus sub-itens, G-008.2, TASK-023).
- Item → Item (via scheduleId): ON DELETE SET NULL — deletar um SCHEDULE não deve impedir nem forçar a deleção do TODO associado; o vínculo simplesmente se desfaz.
Outras observações
- Recorrência (recurrenceFrequency, recurrenceEndDate) não gera tabela de instâncias — cálculo sob demanda via rrule, decisão já registrada em ADR-013 (Arquitetura); os dois campos aqui apenas persistem a regra-base.
- Sessao.revokedAt nulo = sessão ativa; logout individual (RF-021) seta revokedAt; revogação em massa (ADR-007) atualiza todas as linhas do userId, exceto a sessão corrente quando aplicável (troca de senha).
- Usuario.failedLoginAttempts/lockedUntil implementam o bloqueio por conta (ADR-009); o throttler por IP (ADR-009) é infraestrutural e não persiste dado neste modelo.