Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,90 @@ Gera uma página para download de um certificado.
- `test_list_user_certificates("suzi.harima94@gmail.com", success="false")`
- Testar e-mail com URL encoding:
- `test_list_user_certificates("user+qa@example.com")`

## DynamoDB Single-Table Design

Este projeto utiliza o padrão DynamoDB Single-Table Design, onde todas as entidades (Orders, Certificates, Products, Participants) são armazenadas em uma única tabela DynamoDB.

### Estrutura da Tabela

| Atributo | Tipo | Descrição |
|----------|------|-----------|
| `PK` | String | Chave de Partição principal |
| `SK` | String | Chave de Ordenação principal |
| `GSI1PK` | String | Chave de Partição do GSI1 |
| `GSI1SK` | String | Chave de Ordenação do GSI1 |
| `GSI2PK` | String | Chave de Partição do GSI2 |
| `GSI2SK` | String | Chave de Ordenação do GSI2 |
| `GSI3PK` | String | Chave de Partição do GSI3 |
| `GSI3SK` | String | Chave de Ordenação do GSI3 |
| `GSI4PK` | String | Chave de Partição do GSI4 |
| `GSI4SK` | String | Chave de Ordenação do GSI4 |
| `EntityType` | String | Tipo da entidade (ORDER, CERTIFICATE, PRODUCT, PARTICIPANT) |

### GSIs (Global Secondary Indexes)

| GSI | Key Schema | Access Pattern |
|-----|------------|----------------|
| GSI1 | `PK: UUID, SK: CERT#` | Certificate by UUID |
| GSI2 | `PK: email, SK: ENTITY#` | Orders, Certificates, Participants by email |
| GSI3 | `PK: product, SK: ENTITY#` | Products by name, Certificates/Orders by product |
| GSI4 | `PK: SUCCESS#Y/N, SK: CERT#` | Successful/Failed certificates |

### Entidades e Keys

#### Certificate
| Key | Value |
|-----|-------|
| PK | `CERTIFICATE#<uuid>` |
| SK | `CERTIFICATE#<uuid>` |
| GSI1PK | `<uuid>` |
| GSI1SK | `CERTIFICATE#<uuid>` |
| GSI2PK | `EMAIL#<normalized_email>` |
| GSI2SK | `CERTIFICATE#<order_id>` |
| GSI3PK | `PRODUCT#<product_id>` |
| GSI3SK | `CERTIFICATE#<order_id>` |
| GSI4PK | `SUCCESS#<true/false>` |
| GSI4SK | `CERTIFICATE#<uuid>` |

#### Order
| Key | Value |
|-----|-------|
| PK | `ORDER#<order_id>` |
| SK | `ORDER#<order_id>` |
| GSI2PK | `EMAIL#<normalized_email>` |
| GSI2SK | `ORDER#<order_id>` |
| GSI3PK | `PRODUCT#<product_id>` |
| GSI3SK | `ORDER#<order_id>` |

#### Product
| Key | Value |
|-----|-------|
| PK | `PRODUCT#<product_id>` |
| SK | `PRODUCT#<product_id>` |
| GSI3PK | `PRODUCT#<product_name>` |
| GSI3SK | `PRODUCT#<product_id>` |

#### Participant
| Key | Value |
|-----|-------|
| PK | `PARTICIPANT#<uuid>` |
| SK | `PARTICIPANT#<uuid>` |
| GSI2PK | `EMAIL#<normalized_email>` |
| GSI2SK | `PARTICIPANT#<uuid>` |

### Normalização de Email

Emails são normalizados para consistência:
- Convertido para minúsculas
- Remoção de pontos antes do `@` (Gmail)
- Remoção de `+` e tudo após

### Repositórios

Cada repositório implementa operações CRUD usando as chaves apropriadas:

- **OrderRepositoryImpl**: GSI2 (email), GSI3 (product)
- **CertificateRepositoryImpl**: GSI1 (UUID), GSI2 (email), GSI3 (product), GSI4 (success)
- **ProductRepositoryImpl**: GSI3 (name)
- **ParticipantRepositoryImpl**: GSI2 (email)
88 changes: 88 additions & 0 deletions src/infrastructure/aws/dynamodb_keys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from enum import Enum
from typing import Optional


class EntityType(str, Enum):
ORDER = "ORDER"
PRODUCT = "PRODUCT"
PARTICIPANT = "PARTICIPANT"
CERTIFICATE = "CERTIFICATE"


def pk(entity_type: EntityType, entity_id: str | int) -> str:
return f"{entity_type.value}#{entity_id}"


def sk(entity_type: EntityType, entity_id: str | int) -> str:
return f"{entity_type.value}#{entity_id}"


def sk_order(order_id: int) -> str:
return sk(EntityType.ORDER, order_id)


def sk_product(product_id: int) -> str:
return sk(EntityType.PRODUCT, product_id)


def sk_participant(participant_id: str) -> str:
return sk(EntityType.PARTICIPANT, participant_id)


def sk_certificate(order_id: int) -> str:
return sk(EntityType.CERTIFICATE, order_id)


def gsi1pk_email(email: str) -> str:
return f"EMAIL#{email.lower().strip()}"


def gsi1sk_participant(participant_id: str) -> str:
return f"PARTICIPANT#{participant_id}"


def gsi1sk_order(order_id: int) -> str:
return f"ORDER#{order_id}"


def gsi1sk_certificate(order_id: int) -> str:
return f"CERTIFICATE#{order_id}"


def gsi2pk_product(product_id: int) -> str:
return f"PRODUCT#{product_id}"


def gsi2sk_order(order_id: int) -> str:
return f"ORDER#{order_id}"


def gsi2sk_certificate(order_id: int) -> str:
return f"CERTIFICATE#{order_id}"


def gsi3pk_success(success: bool) -> str:
return f"SUCCESS#{1 if success else 0}"


def gsi3sk_certificate(order_id: int) -> str:
return f"CERTIFICATE#{order_id}"


def gsi1pk_cert_id(cert_id: str) -> str:
return f"CERT#{cert_id}"


def gsi1sk_cert(cert_id: str) -> str:
return f"CERTIFICATE#{cert_id}"


def parse_pk(pk: str) -> tuple[EntityType, str]:
parts = pk.split("#", 1)
if len(parts) != 2:
raise ValueError(f"Invalid PK format: {pk}")
return EntityType(parts[0]), parts[1]


def parse_sk(sk: str) -> tuple[EntityType, str]:
return parse_pk(sk)
62 changes: 13 additions & 49 deletions src/infrastructure/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,64 +18,28 @@ class Config:
env_file_encoding = "utf-8"

@property
def dynamodb_tables(self) -> Dict[str, Dict[str, str]]:
def dynamodb_table(self) -> Dict[str, str]:
"""
Retorna as configurações das tabelas do DynamoDB baseadas no ambiente.
Segue o padrão da infraestrutura Terraform criada.
Retorna a configuração da tabela única do DynamoDB baseada no ambiente.
Single Table Design - todas as entidades em uma única tabela.
"""
base_name = f"{self.PROJECT_NAME}"
environment = self.ENVIRONMENT

return {
"certificates": {
"name": f"{base_name}-certificates-{environment}",
"arn": f"arn:aws:dynamodb:{self.REGION}:*:table/{base_name}-certificates-{environment}"
},
"orders": {
"name": f"{base_name}-orders-{environment}",
"arn": f"arn:aws:dynamodb:{self.REGION}:*:table/{base_name}-orders-{environment}"
},
"participants": {
"name": f"{base_name}-participants-{environment}",
"arn": f"arn:aws:dynamodb:{self.REGION}:*:table/{base_name}-participants-{environment}"
},
"products": {
"name": f"{base_name}-products-{environment}",
"arn": f"arn:aws:dynamodb:{self.REGION}:*:table/{base_name}-products-{environment}"
}
"name": f"{base_name}-{environment}",
"arn": f"arn:aws:dynamodb:{self.REGION}:*:table/{base_name}-{environment}"
}

def get_table_name(self, entity: str) -> str:
"""
Retorna o nome da tabela para uma entidade específica.

Args:
entity: Nome da entidade (certificates, orders, participants, products)

Returns:
str: Nome da tabela no DynamoDB
"""
tables = self.dynamodb_tables
if entity not in tables:
raise ValueError(f"Entidade '{entity}' não encontrada. Entidades disponíveis: {list(tables.keys())}")

return tables[entity]["name"]
@property
def dynamodb_tables(self) -> Dict[str, Dict[str, str]]:
return {"single": self.dynamodb_table}

def get_table_arn(self, entity: str) -> str:
"""
Retorna o ARN da tabela para uma entidade específica.

Args:
entity: Nome da entidade (certificates, orders, participants, products)

Returns:
str: ARN da tabela no DynamoDB
"""
tables = self.dynamodb_tables
if entity not in tables:
raise ValueError(f"Entidade '{entity}' não encontrada. Entidades disponíveis: {list(tables.keys())}")

return tables[entity]["arn"]
def get_table_name(self, entity: str = None) -> str:
return self.dynamodb_table["name"]

def get_table_arn(self, entity: str = None) -> str:
return self.dynamodb_table["arn"]


config = Config()
Expand Down
8 changes: 4 additions & 4 deletions src/infrastructure/container/dependency_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,22 +104,22 @@ def _create_dynamodb_service(self) -> DynamoDBService:
def _create_certificate_repository(self) -> CertificateRepositoryImpl:
"""Cria uma instância do CertificateRepositoryImpl."""
dynamodb_service = self.get('dynamodb_service')
return CertificateRepositoryImpl(dynamodb_service, "certificates")
return CertificateRepositoryImpl(dynamodb_service)

def _create_participant_repository(self) -> ParticipantRepositoryImpl:
"""Cria uma instância do ParticipantRepositoryImpl."""
dynamodb_service = self.get('dynamodb_service')
return ParticipantRepositoryImpl(dynamodb_service, "participants")
return ParticipantRepositoryImpl(dynamodb_service)

def _create_product_repository(self) -> ProductRepositoryImpl:
"""Cria uma instância do ProductRepositoryImpl."""
dynamodb_service = self.get('dynamodb_service')
return ProductRepositoryImpl(dynamodb_service, "products")
return ProductRepositoryImpl(dynamodb_service)

def _create_order_repository(self) -> OrderRepositoryImpl:
"""Cria uma instância do OrderRepositoryImpl."""
dynamodb_service = self.get('dynamodb_service')
return OrderRepositoryImpl(dynamodb_service, "orders")
return OrderRepositoryImpl(dynamodb_service)

def _create_send_for_build_certificate(self):
"""
Expand Down
Loading