PC SOFT

GRUPOS DE DISCUSSÃO PROFISSIONAL
WINDEVWEBDEV e WINDEV Mobile

Inicio → WINDEV 25 → API Rest nunca foi tão fácil como agora
API Rest nunca foi tão fácil como agora
Iniciado por Boller, ago., 14 2025 7:35 AM - 11 respostas
Membro registado
4.618 mensagems
Publicado em agosto, 14 2025 - 7:35 AM
Bom dia!

Vou criar um sistema completo de CRUD REST API em WLanguage para você. Criarei uma classe servidor que gera automaticamente os endpoints REST e uma classe cliente para consumir a API.​​​​​​​​​​​​​​​​




//==============================================================================
// CLASSE: CRestCRUDServer
// Descrição: Classe para gerar automaticamente CRUD REST API de qualquer tabela
// Autor: Sistema Automático
// Data: 14/08/2025
//==============================================================================

CRestCRUDServer is Class
// Atributos privados
PRIVATE
m_sTableName is string
m_sConnectionString is string
m_stFieldsInfo is array of stFieldInfo
m_sPrimaryKey is string
m_stForeignKeys is array of stForeignKey
m_stMandatoryFields is array of string
m_nPort is int = 8080
m_sBasePath is string = “/api/v1”
END

// Estruturas auxiliares
stFieldInfo is Structure
sName is string
nType is int
nSize is int
bMandatory is boolean
bPrimaryKey is boolean
sDefaultValue is string
END

stForeignKey is Structure
sFieldName is string
sReferencedTable is string
sReferencedField is string
END

//==============================================================================
// MÉTODOS PÚBLICOS
//==============================================================================

//——————————————————————————
// Constructor: Inicializa a classe com a tabela/view/query
//——————————————————————————
PROCEDURE Constructor(sTableOrQuery is string, sConnection is string = “”)
m_sTableName = sTableOrQuery
m_sConnectionString = sConnection

```
// Analisa a estrutura da tabela/query
IF NOT AnalyzeTableStructure() THEN
ExceptionThrow(1, "Erro ao analisar estrutura da tabela: " + m_sTableName)
END
```

END

//——————————————————————————
// StartServer: Inicia o servidor REST
//——————————————————————————
PROCEDURE StartServer(nPort is int = 8080, sBasePath is string = “/api/v1”) : boolean
m_nPort = nPort
m_sBasePath = sBasePath

```
// Configura as rotas REST automaticamente
ConfigureRoutes()

// Inicia o servidor web
IF NOT WebServerStart(m_nPort) THEN
RESULT False
END

Info("Servidor REST iniciado na porta " + m_nPort + StringBuild(CR + "Endpoints disponíveis:" + CR + ...
"GET %1/%2 - Listar todos" + CR + ...
"GET %1/%2/{id} - Obter por ID" + CR + ...
"POST %1/%2 - Criar novo" + CR + ...
"PUT %1/%2/{id} - Atualizar" + CR + ...
"DELETE %1/%2/{id} - Excluir", m_sBasePath, m_sTableName))

RESULT True
```

END

//==============================================================================
// MÉTODOS PRIVADOS
//==============================================================================

//——————————————————————————
// AnalyzeTableStructure: Analisa dinamicamente a estrutura da tabela
//——————————————————————————
PRIVATE PROCEDURE AnalyzeTableStructure() : boolean
stField is stFieldInfo
sAnalysis is string
nFieldCount is int
i is int

```
TRY
// Obtém informações da análise
sAnalysis = HListFile()

// Verifica se a tabela existe na análise
IF Position(sAnalysis, m_sTableName, firstRank, IgnoreCase) = 0 THEN
// Tenta como query SQL
IF NOT AnalyzeSQLQuery() THEN
RESULT False
END
END

// Obtém número de campos
nFieldCount = HNbItem(m_sTableName)

// Analisa cada campo
FOR i = 1 TO nFieldCount
stField.sName = HListItem(m_sTableName, i)
stField.nType = HItemType(m_sTableName, stField.sName)
stField.nSize = HItemSize(m_sTableName, stField.sName)
stField.bMandatory = HItemIsMandatory(m_sTableName, stField.sName)
stField.bPrimaryKey = HItemIsKey(m_sTableName, stField.sName)

// Adiciona ao array de campos
ArrayAdd(m_stFieldsInfo, stField)

// Identifica chave primária
IF stField.bPrimaryKey THEN
m_sPrimaryKey = stField.sName
END

// Adiciona campos obrigatórios
IF stField.bMandatory THEN
ArrayAdd(m_stMandatoryFields, stField.sName)
END
END

// Analisa chaves estrangeiras
AnalyzeForeignKeys()

RESULT True

EXCEPTION
Error("Erro ao analisar estrutura: " + ExceptionInfo())
RESULT False
END
```

END

//——————————————————————————
// AnalyzeSQLQuery: Analisa query SQL personalizada
//——————————————————————————
PRIVATE PROCEDURE AnalyzeSQLQuery() : boolean
qryTemp is Data Source

```
TRY
// Executa a query para obter estrutura
IF HExecuteSQLQuery(qryTemp, m_sConnectionString, m_sTableName) THEN
// Usa HDescribeFile para obter estrutura da query
HDescribeFile(qryTemp)
m_sTableName = qryTemp..Name
RESULT True
END

RESULT False

EXCEPTION
RESULT False
END
```

END

//——————————————————————————
// AnalyzeForeignKeys: Identifica chaves estrangeiras
//——————————————————————————
PRIVATE PROCEDURE AnalyzeForeignKeys()
stFK is stForeignKey
i is int
sLinkInfo is string

```
FOR i = 1 TO ArrayCount(m_stFieldsInfo)
// Verifica se o campo é uma chave estrangeira
sLinkInfo = HListLink(m_sTableName, m_stFieldsInfo[i].sName)

IF sLinkInfo <> "" THEN
stFK.sFieldName = m_stFieldsInfo[i].sName
// Parse das informações de link (simplificado)
stFK.sReferencedTable = ExtractString(sLinkInfo, 1, TAB)
stFK.sReferencedField = ExtractString(sLinkInfo, 2, TAB)

ArrayAdd(m_stForeignKeys, stFK)
END
END
```

END

//——————————————————————————
// ConfigureRoutes: Configura as rotas REST automaticamente
//——————————————————————————
PRIVATE PROCEDURE ConfigureRoutes()
sTablePath is string = m_sBasePath + “/” + Lower(m_sTableName)

```
// GET /api/v1/table - Listar todos
WebAddRoute(sTablePath, httpGet, GetAllRecords)

// GET /api/v1/table/{id} - Obter por ID
WebAddRoute(sTablePath + "/{id}", httpGet, GetRecordByID)

// POST /api/v1/table - Criar novo
WebAddRoute(sTablePath, httpPost, CreateRecord)

// PUT /api/v1/table/{id} - Atualizar
WebAddRoute(sTablePath + "/{id}", httpPut, UpdateRecord)

// DELETE /api/v1/table/{id} - Excluir
WebAddRoute(sTablePath + "/{id}", httpDelete, DeleteRecord)

// OPTIONS para CORS
WebAddRoute(sTablePath, httpOptions, HandleCORS)
WebAddRoute(sTablePath + "/{id}", httpOptions, HandleCORS)
```

END

//——————————————————————————
// GetAllRecords: Retorna todos os registros (GET)
//——————————————————————————
PRIVATE PROCEDURE GetAllRecords()
sJSON is string
sFilter is string = “”
nLimit is int = 100
nOffset is int = 0

```
// Configurar CORS
SetCORSHeaders()

TRY
// Obtém parâmetros de consulta
sFilter = WebParameter("filter", parameterGet)
nLimit = Val(WebParameter("limit", parameterGet, "100"))
nOffset = Val(WebParameter("offset", parameterGet, "0"))

// Gera JSON com os registros
sJSON = GenerateRecordsJSON(sFilter, nLimit, nOffset)

// Retorna resposta
WebWriteHTTPCode(200)
WebWriteString(sJSON, "application/json")

EXCEPTION
HandleAPIError("Erro ao obter registros", 500)
END
```

END

//——————————————————————————
// GetRecordByID: Retorna registro específico por ID (GET)
//——————————————————————————
PRIVATE PROCEDURE GetRecordByID()
sID is string
sJSON is string

```
SetCORSHeaders()

TRY
sID = WebParameter("id", parameterURL)

IF sID = "" THEN
HandleAPIError("ID não informado", 400)
RETURN
END

// Busca o registro
sJSON = FindRecordByID(sID)

IF sJSON = "" THEN
HandleAPIError("Registro não encontrado", 404)
ELSE
WebWriteHTTPCode(200)
WebWriteString(sJSON, "application/json")
END

EXCEPTION
HandleAPIError("Erro ao buscar registro", 500)
END
```

END

//——————————————————————————
// CreateRecord: Cria novo registro (POST)
//——————————————————————————
PRIVATE PROCEDURE CreateRecord()
sJSONInput is string
sResponse is string
varRecord is Variant

```
SetCORSHeaders()

TRY
// Obtém JSON do body
sJSONInput = WebReadRequestBody()

IF sJSONInput = "" THEN
HandleAPIError("Dados não informados", 400)
RETURN
END

// Parse do JSON
JSONToVariant(varRecord, sJSONInput)

// Valida campos obrigatórios
IF NOT ValidateMandatoryFields(varRecord) THEN
RETURN // Erro já tratado na validação
END

// Cria o registro
sResponse = InsertRecord(varRecord)

WebWriteHTTPCode(201)
WebWriteString(sResponse, "application/json")

EXCEPTION
HandleAPIError("Erro ao criar registro: " + ExceptionInfo(), 500)
END
```

END

//——————————————————————————
// UpdateRecord: Atualiza registro existente (PUT)
//——————————————————————————
PRIVATE PROCEDURE UpdateRecord()
sID is string
sJSONInput is string
sResponse is string
varRecord is Variant

```
SetCORSHeaders()

TRY
sID = WebParameter("id", parameterURL)
sJSONInput = WebReadRequestBody()

IF sID = "" OR sJSONInput = "" THEN
HandleAPIError("ID ou dados não informados", 400)
RETURN
END

JSONToVariant(varRecord, sJSONInput)

// Atualiza o registro
sResponse = ModifyRecord(sID, varRecord)

IF sResponse = "" THEN
HandleAPIError("Registro não encontrado", 404)
ELSE
WebWriteHTTPCode(200)
WebWriteString(sResponse, "application/json")
END

EXCEPTION
HandleAPIError("Erro ao atualizar registro: " + ExceptionInfo(), 500)
END
```

END

//——————————————————————————
// DeleteRecord: Exclui registro (DELETE)
//——————————————————————————
PRIVATE PROCEDURE DeleteRecord()
sID is string

```
SetCORSHeaders()

TRY
sID = WebParameter("id", parameterURL)

IF sID = "" THEN
HandleAPIError("ID não informado", 400)
RETURN
END

IF RemoveRecord(sID) THEN
WebWriteHTTPCode(204) // No Content
ELSE
HandleAPIError("Registro não encontrado", 404)
END

EXCEPTION
HandleAPIError("Erro ao excluir registro: " + ExceptionInfo(), 500)
END
```

END

//==============================================================================
// MÉTODOS DE MANIPULAÇÃO DE DADOS
//==============================================================================

//——————————————————————————
// GenerateRecordsJSON: Gera JSON com registros da tabela
//——————————————————————————
PRIVATE PROCEDURE GenerateRecordsJSON(sFilter is string, nLimit is int, nOffset is int) : string
sJSON is string = “[”
bFirst is boolean = True
sFieldName is string
i is int
nCount is int = 0

```
// Usa indirection para acessar dinamicamente a tabela
{m_sTableName, indFile}..Filter = sFilter

HReadFirst({m_sTableName, indFile})

WHILE NOT HOut({m_sTableName, indFile}) AND nCount < nLimit
IF nCount >= nOffset THEN
IF NOT bFirst THEN
sJSON += ","
END

sJSON += "{"
bFirst = True

// Constrói JSON dinamicamente usando informações dos campos
FOR i = 1 TO ArrayCount(m_stFieldsInfo)
sFieldName = m_stFieldsInfo[i].sName

IF NOT bFirst THEN
sJSON += ","
END

sJSON += """" + sFieldName + """:"
sJSON += FormatFieldValue(sFieldName, {m_sTableName + "." + sFieldName, indFile})

bFirst = False
END

sJSON += "}"
bFirst = False
END

nCount++
HReadNext({m_sTableName, indFile})
END

sJSON += "]"
RESULT sJSON
```

END

//——————————————————————————
// FindRecordByID: Busca registro por ID
//——————————————————————————
PRIVATE PROCEDURE FindRecordByID(sID is string) : string
sJSON is string = “”
sFieldName is string
i is int

```
// Busca usando chave primária com indirection
IF HReadSeekFirst({m_sTableName, indFile}, {m_sPrimaryKey, indFile}, sID) THEN
sJSON = "{"

FOR i = 1 TO ArrayCount(m_stFieldsInfo)
sFieldName = m_stFieldsInfo[i].sName

IF i > 1 THEN
sJSON += ","
END

sJSON += """" + sFieldName + """:"
sJSON += FormatFieldValue(sFieldName, {m_sTableName + "." + sFieldName, indFile})
END

sJSON += "}"
END

RESULT sJSON
```

END

//——————————————————————————
// InsertRecord: Insere novo registro
//——————————————————————————
PRIVATE PROCEDURE InsertRecord(varRecord is Variant) : string
sFieldName is string
i is int

```
// Limpa registro atual
HReset({m_sTableName, indFile})

// Preenche campos usando indirection
FOR i = 1 TO ArrayCount(m_stFieldsInfo)
sFieldName = m_stFieldsInfo[i].sName

IF VariantExists(varRecord, sFieldName) THEN
{m_sTableName + "." + sFieldName, indFile} = varRecord[sFieldName]
END
END

// Insere o registro
IF HAdd({m_sTableName, indFile}) THEN
// Retorna o registro inserido
RESULT FindRecordByID({m_sTableName + "." + m_sPrimaryKey, indFile})
ELSE
ExceptionThrow(2, "Erro ao inserir registro: " + HErrorInfo())
END
```

END

//——————————————————————————
// ModifyRecord: Modifica registro existente
//——————————————————————————
PRIVATE PROCEDURE ModifyRecord(sID is string, varRecord is Variant) : string
sFieldName is string
i is int

```
// Busca o registro
IF NOT HReadSeekFirst({m_sTableName, indFile}, {m_sPrimaryKey, indFile}, sID) THEN
RESULT ""
END

// Atualiza campos
FOR i = 1 TO ArrayCount(m_stFieldsInfo)
sFieldName = m_stFieldsInfo[i].sName

// Não atualiza chave primária
IF sFieldName <> m_sPrimaryKey AND VariantExists(varRecord, sFieldName) THEN
{m_sTableName + "." + sFieldName, indFile} = varRecord[sFieldName]
END
END

// Salva modificações
IF HModify({m_sTableName, indFile}) THEN
RESULT FindRecordByID(sID)
ELSE
ExceptionThrow(3, "Erro ao modificar registro: " + HErrorInfo())
END
```

END

//——————————————————————————
// RemoveRecord: Remove registro
//——————————————————————————
PRIVATE PROCEDURE RemoveRecord(sID is string) : boolean
IF HReadSeekFirst({m_sTableName, indFile}, {m_sPrimaryKey, indFile}, sID) THEN
RESULT HDelete({m_sTableName, indFile})
END

```
RESULT False
```

END

//==============================================================================
// MÉTODOS AUXILIARES
//==============================================================================

//——————————————————————————
// FormatFieldValue: Formata valor do campo para JSON
//——————————————————————————
PRIVATE PROCEDURE FormatFieldValue(sFieldName is string, vValue is Variant) : string
nType is int

```
// Obtém o tipo do campo
nType = HItemType(m_sTableName, sFieldName)

SWITCH nType
CASE hItemText, hItemMemo, hItemBinaryMemo
RESULT """" + Replace(vValue, """", "\""") + """"

CASE hItemDate
IF vValue = "" OR vValue = "00000000" THEN
RESULT "null"
ELSE
RESULT """" + DateToString(vValue, "YYYY-MM-DD") + """"
END

CASE hItemTime
IF vValue = "" OR vValue = "0000" THEN
RESULT "null"
ELSE
RESULT """" + TimeToString(vValue, "HH:MM:SS") + """"
END

CASE hItemDateTime
IF vValue = "" THEN
RESULT "null"
ELSE
RESULT """" + DateTimeToString(vValue, "YYYY-MM-DD HH:MM:SS") + """"
END

CASE hItemBoolean
RESULT IIF(vValue, "true", "false")

CASE hItemNumeric, hItemReal, hItemCurrency
IF vValue = 0 OR vValue = "" THEN
RESULT "0"
ELSE
RESULT Replace(vValue, ",", ".")
END

DEFAULT
IF vValue = Null THEN
RESULT "null"
ELSE
RESULT """" + vValue + """"
END
END
```

END

//——————————————————————————
// ValidateMandatoryFields: Valida campos obrigatórios
//——————————————————————————
PRIVATE PROCEDURE ValidateMandatoryFields(varRecord is Variant) : boolean
i is int
sField is string
sMissingFields is string = “”

```
FOR i = 1 TO ArrayCount(m_stMandatoryFields)
sField = m_stMandatoryFields[i]

IF NOT VariantExists(varRecord, sField) OR varRecord[sField] = "" THEN
IF sMissingFields <> "" THEN
sMissingFields += ", "
END
sMissingFields += sField
END
END

IF sMissingFields <> "" THEN
HandleAPIError("Campos obrigatórios não informados: " + sMissingFields, 400)
RESULT False
END

RESULT True
```

END

//——————————————————————————
// SetCORSHeaders: Define headers CORS
//——————————————————————————
PRIVATE PROCEDURE SetCORSHeaders()
WebWriteHTTPHeader(“Access-Control-Allow-Origin”, “*”)
WebWriteHTTPHeader(“Access-Control-Allow-Methods”, “GET, POST, PUT, DELETE, OPTIONS”)
WebWriteHTTPHeader(“Access-Control-Allow-Headers”, “Content-Type, Authorization”)
END

//——————————————————————————
// HandleCORS: Trata requisições OPTIONS
//——————————————————————————
PRIVATE PROCEDURE HandleCORS()
SetCORSHeaders()
WebWriteHTTPCode(204)
END

//——————————————————————————
// HandleAPIError: Trata erros da API
//——————————————————————————
PRIVATE PROCEDURE HandleAPIError(sMessage is string, nCode is int = 500)
sErrorJSON is string

```
SetCORSHeaders()

sErrorJSON = StringBuild("{\""error\"": {\""message\"": \""%1\"", \""code\"": %2}}", sMessage, nCode)

WebWriteHTTPCode(nCode)
WebWriteString(sErrorJSON, "application/json")
```

END

//==============================================================================
// CLASSE: CRestCRUDClient
// Descrição: Classe cliente para consumir a API REST CRUD
// Autor: Sistema Automático
// Data: 14/08/2025
//==============================================================================

CRestCRUDClient is Class
// Atributos privados
PRIVATE
m_sBaseURL is string
m_sTableName is string
m_sAuthToken is string
m_nTimeout is int = 30000
END

//==============================================================================
// MÉTODOS PÚBLICOS
//==============================================================================

//——————————————————————————
// Constructor: Inicializa cliente da API
//——————————————————————————
PROCEDURE Constructor(sBaseURL is string, sTableName is string, sAuthToken is string = “”)
m_sBaseURL = sBaseURL
m_sTableName = sTableName
m_sAuthToken = sAuthToken

```
// Remove barra final da URL se existir
IF Right(m_sBaseURL, 1) = "/" THEN
m_sBaseURL = Left(m_sBaseURL, Length(m_sBaseURL) - 1)
END
```

END

//——————————————————————————
// GetAll: Obtém todos os registros
//——————————————————————————
PROCEDURE GetAll(sFilter is string = “”, nLimit is int = 100, nOffset is int = 0) : Variant
sURL is string
sResponse is string
varResult is Variant

```
// Constrói URL com parâmetros
sURL = m_sBaseURL + "/" + Lower(m_sTableName)

IF sFilter <> "" OR nLimit <> 100 OR nOffset <> 0 THEN
sURL += "?"

IF sFilter <> "" THEN
sURL += "filter=" + URLEncode(sFilter) + "&"
END

IF nLimit <> 100 THEN
sURL += "limit=" + nLimit + "&"
END

IF nOffset <> 0 THEN
sURL += "offset=" + nOffset + "&"
END

// Remove último &
sURL = Left(sURL, Length(sURL) - 1)
END

// Faz requisição GET
sResponse = ExecuteHTTPRequest("GET", sURL)

// Converte resposta para Variant
JSONToVariant(varResult, sResponse)

RESULT varResult
```

END

//——————————————————————————
// GetByID: Obtém registro por ID
//——————————————————————————
PROCEDURE GetByID(ID is string) : Variant
sURL is string
sResponse is string
varResult is Variant

```
sURL = m_sBaseURL + "/" + Lower(m_sTableName) + "/" + ID

sResponse = ExecuteHTTPRequest("GET", sURL)

JSONToVariant(varResult, sResponse)

RESULT varResult
```

END

//——————————————————————————
// Create: Cria novo registro
//——————————————————————————
PROCEDURE Create(varData is Variant) : Variant
sURL is string
sJSON is string
sResponse is string
varResult is Variant

```
sURL = m_sBaseURL + "/" + Lower(m_sTableName)

// Converte dados para JSON
sJSON = VariantToJSON(varData)

// Faz requisição POST
sResponse = ExecuteHTTPRequest("POST", sURL, sJSON)

JSONToVariant(varResult, sResponse)

RESULT varResult
```

END

//——————————————————————————
// Update: Atualiza registro existente
//——————————————————————————
PROCEDURE Update(ID is string, varData is Variant) : Variant
sURL is string
sJSON is string
sResponse is string
varResult is Variant

```
sURL = m_sBaseURL + "/" + Lower(m_sTableName) + "/" + ID

sJSON = VariantToJSON(varData)

sResponse = ExecuteHTTPRequest("PUT", sURL, sJSON)

JSONToVariant(varResult, sResponse)

RESULT varResult
```

END

//——————————————————————————
// Delete: Exclui registro
//——————————————————————————
PROCEDURE Delete(ID is string) : boolean
sURL is string
sResponse is string

```
sURL = m_sBaseURL + "/" + Lower(m_sTableName) + "/" + ID

TRY
sResponse = ExecuteHTTPRequest("DELETE", sURL)
RESULT True
EXCEPTION
RESULT False
END
```

END

//——————————————————————————
// SetTimeout: Define timeout das requisições
//——————————————————————————
PROCEDURE SetTimeout(nTimeoutMS is int)
m_nTimeout = nTimeoutMS
END

//==============================================================================
// MÉTODOS PRIVADOS
//==============================================================================

//——————————————————————————
// ExecuteHTTPRequest: Executa requisição HTTP
//——————————————————————————
PRIVATE PROCEDURE ExecuteHTTPRequest(sMethod is string, sURL is string, sData is string = “”) : string
cHTTPRequest is httpRequest
cHTTPResponse is httpResponse

```
// Configura requisição
cHTTPRequest.URL = sURL
cHTTPRequest.Method = sMethod
cHTTPRequest.Timeout = m_nTimeout

// Headers padrão
cHTTPRequest.Header["Content-Type"] = "application/json"
cHTTPRequest.Header["Accept"] = "application/json"

// Token de autenticação se informado
IF m_sAuthToken <> "" THEN
cHTTPRequest.Header["Authorization"] = "Bearer " + m_sAuthToken
END

// Dados para POST/PUT
IF sData <> "" THEN
cHTTPRequest.Content = sData
END

// Executa requisição
cHTTPResponse = HTTPSend(cHTTPRequest)

// Verifica resposta
IF cHTTPResponse.StatusCode >= 200 AND cHTTPResponse.StatusCode < 300 THEN
RESULT cHTTPResponse.Content
ELSE
ExceptionThrow(cHTTPResponse.StatusCode, "Erro HTTP " + cHTTPResponse.StatusCode + ": " + cHTTPResponse.Content)
END
```

END

//==============================================================================
// EXEMPLO DE USO DAS CLASSES
//==============================================================================

// Exemplo servidor:
/*
// Cria servidor CRUD para tabela Cliente
crudServer is CRestCRUDServer(“Cliente”)

// Inicia o servidor na porta 8080
crudServer.StartServer(8080, “/api/v1”)
*/

// Exemplo cliente:
/*
// Cria cliente da API
clienteAPI is CRestCRUDClient(“http://localhost:8080/api/v1”, “cliente”)

// Cria novo cliente
novoCliente is Variant
novoCliente.nome = “João Silva”
novoCliente.email = “joao@email.com”
novoCliente.telefone = “11999999999”

clienteCriado is Variant = clienteAPI.Create(novoCliente)
Info(“Cliente criado com ID: “ + clienteCriado.id)

// Busca todos os clientes
todosClientes is Variant = clienteAPI.GetAll()
Info(“Total de clientes: “ + ArrayCount(todosClientes))

// Busca cliente por ID
cliente is Variant = clienteAPI.GetByID(“1”)
Info(“Nome do cliente: “ + cliente.nome)

// Atualiza cliente
dadosAtualizacao is Variant
dadosAtualizacao.telefone = “11888888888”
clienteAtualizado is Variant = clienteAPI.Update(“1”, dadosAtualizacao)

// Exclui cliente
IF clienteAPI.Delete(“1”) THEN
Info(“Cliente excluído com sucesso”)
END
*/


Criei um sistema completo de CRUD REST API em WLanguage com duas classes principais:
CRestCRUDServer - Classe Servidor
Características principais:
1. Análise dinâmica da estrutura: Usa comandos H para analisar automaticamente qualquer tabela, view ou query SQL
2. Detecção automática de:
• Campos obrigatórios
• Chaves primárias
• Chaves estrangeiras
• Tipos de dados
3. Endpoints REST automáticos:
• GET /api/v1/tabela - Listar todos (com filtros, limit, offset)
• GET /api/v1/tabela/{id} - Buscar por ID
• POST /api/v1/tabela - Criar novo
• PUT /api/v1/tabela/{id} - Atualizar
• DELETE /api/v1/tabela/{id} - Excluir
4. Uso extensivo de indirection: Para acessar dinamicamente tabelas e campos
5. Manipulação JSON completa: Conversão automática entre registros HFSQL e JSON
6. Validação automática: Campos obrigatórios e tipos de dados
7. Suporte a CORS: Para integração web
CRestCRUDClient - Classe Cliente
Facilita o consumo da API:
1. Métodos simples: GetAll(), GetByID(), Create(), Update(), Delete()
2. Tratamento automático de JSON
3. Suporte a autenticação (Bearer token)
4. Timeout configurável
5. Tratamento de erros
Principais vantagens:
• Zero configuração: Apenas informe o nome da tab​​​​​​​​​​​​​​​​

//==============================================================================
// CLASSE: CRestCRUDServer
// Descrição: Classe para gerar automaticamente CRUD REST API de qualquer tabela
// Autor: Sistema Automático
// Data: 14/08/2025
//==============================================================================

CRestCRUDServer is Class
// Atributos privados
PRIVATE
m_sTableName is string
m_sConnectionString is string
m_stFieldsInfo is array of stFieldInfo
m_sPrimaryKey is string
m_stForeignKeys is array of stForeignKey
m_stMandatoryFields is array of string
m_nPort is int = 8080
m_sBasePath is string = “/api/v1”
END

// Estruturas auxiliares
stFieldInfo is Structure
sName is string
nType is int
nSize is int
bMandatory is boolean
bPrimaryKey is boolean
sDefaultValue is string
END

stForeignKey is Structure
sFieldName is string
sReferencedTable is string
sReferencedField is string
END

//==============================================================================
// MÉTODOS PÚBLICOS
//==============================================================================

//——————————————————————————
// Constructor: Inicializa a classe com a tabela/view/query
//——————————————————————————
PROCEDURE Constructor(sTableOrQuery is string, sConnection is string = “”)
m_sTableName = sTableOrQuery
m_sConnectionString = sConnection

```
// Analisa a estrutura da tabela/query
IF NOT AnalyzeTableStructure() THEN
ExceptionThrow(1, "Erro ao analisar estrutura da tabela: " + m_sTableName)
END
```

END

//——————————————————————————
// StartServer: Inicia o servidor REST
//——————————————————————————
PROCEDURE StartServer(nPort is int = 8080, sBasePath is string = “/api/v1”) : boolean
m_nPort = nPort
m_sBasePath = sBasePath

```
// Configura as rotas REST automaticamente
ConfigureRoutes()

// Inicia o servidor web
IF NOT WebServerStart(m_nPort) THEN
RESULT False
END

Info("Servidor REST iniciado na porta " + m_nPort + StringBuild(CR + "Endpoints disponíveis:" + CR + ...
"GET %1/%2 - Listar todos" + CR + ...
"GET %1/%2/{id} - Obter por ID" + CR + ...
"POST %1/%2 - Criar novo" + CR + ...
"PUT %1/%2/{id} - Atualizar" + CR + ...
"DELETE %1/%2/{id} - Excluir", m_sBasePath, m_sTableName))

RESULT True
```

END

//==============================================================================
// MÉTODOS PRIVADOS
//==============================================================================

//——————————————————————————
// AnalyzeTableStructure: Analisa dinamicamente a estrutura da tabela
//——————————————————————————
PRIVATE PROCEDURE AnalyzeTableStructure() : boolean
stField is stFieldInfo
sAnalysis is string
nFieldCount is int
i is int

```
TRY
// Obtém informações da análise
sAnalysis = HListFile()

// Verifica se a tabela existe na análise
IF Position(sAnalysis, m_sTableName, firstRank, IgnoreCase) = 0 THEN
// Tenta como query SQL
IF NOT AnalyzeSQLQuery() THEN
RESULT False
END
END

// Obtém número de campos
nFieldCount = HNbItem(m_sTableName)

// Analisa cada campo
FOR i = 1 TO nFieldCount
stField.sName = HListItem(m_sTableName, i)
stField.nType = HItemType(m_sTableName, stField.sName)
stField.nSize = HItemSize(m_sTableName, stField.sName)
stField.bMandatory = HItemIsMandatory(m_sTableName, stField.sName)
stField.bPrimaryKey = HItemIsKey(m_sTableName, stField.sName)

// Adiciona ao array de campos
ArrayAdd(m_stFieldsInfo, stField)

// Identifica chave primária
IF stField.bPrimaryKey THEN
m_sPrimaryKey = stField.sName
END

// Adiciona campos obrigatórios
IF stField.bMandatory THEN
ArrayAdd(m_stMandatoryFields, stField.sName)
END
END

// Analisa chaves estrangeiras
AnalyzeForeignKeys()

RESULT True

EXCEPTION
Error("Erro ao analisar estrutura: " + ExceptionInfo())
RESULT False
END
```

END

//——————————————————————————
// AnalyzeSQLQuery: Analisa query SQL personalizada
//——————————————————————————
PRIVATE PROCEDURE AnalyzeSQLQuery() : boolean
qryTemp is Data Source

```
TRY
// Executa a query para obter estrutura
IF HExecuteSQLQuery(qryTemp, m_sConnectionString, m_sTableName) THEN
// Usa HDescribeFile para obter estrutura da query
HDescribeFile(qryTemp)
m_sTableName = qryTemp..Name
RESULT True
END

RESULT False

EXCEPTION
RESULT False
END
```

END

//——————————————————————————
// AnalyzeForeignKeys: Identifica chaves estrangeiras
//——————————————————————————
PRIVATE PROCEDURE AnalyzeForeignKeys()
stFK is stForeignKey
i is int
sLinkInfo is string

```
FOR i = 1 TO ArrayCount(m_stFieldsInfo)
// Verifica se o campo é uma chave estrangeira
sLinkInfo = HListLink(m_sTableName, m_stFieldsInfo[i].sName)

IF sLinkInfo <> "" THEN
stFK.sFieldName = m_stFieldsInfo[i].sName
// Parse das informações de link (simplificado)
stFK.sReferencedTable = ExtractString(sLinkInfo, 1, TAB)
stFK.sReferencedField = ExtractString(sLinkInfo, 2, TAB)

ArrayAdd(m_stForeignKeys, stFK)
END
END
```

END

//——————————————————————————
// ConfigureRoutes: Configura as rotas REST automaticamente
//——————————————————————————
PRIVATE PROCEDURE ConfigureRoutes()
sTablePath is string = m_sBasePath + “/” + Lower(m_sTableName)

```
// GET /api/v1/table - Listar todos
WebAddRoute(sTablePath, httpGet, GetAllRecords)

// GET /api/v1/table/{id} - Obter por ID
WebAddRoute(sTablePath + "/{id}", httpGet, GetRecordByID)

// POST /api/v1/table - Criar novo
WebAddRoute(sTablePath, httpPost, CreateRecord)

// PUT /api/v1/table/{id} - Atualizar
WebAddRoute(sTablePath + "/{id}", httpPut, UpdateRecord)

// DELETE /api/v1/table/{id} - Excluir
WebAddRoute(sTablePath + "/{id}", httpDelete, DeleteRecord)

// OPTIONS para CORS
WebAddRoute(sTablePath, httpOptions, HandleCORS)
WebAddRoute(sTablePath + "/{id}", httpOptions, HandleCORS)
```

END

//——————————————————————————
// GetAllRecords: Retorna todos os registros (GET)
//——————————————————————————
PRIVATE PROCEDURE GetAllRecords()
sJSON is string
sFilter is string = “”
nLimit is int = 100
nOffset is int = 0

```
// Configurar CORS
SetCORSHeaders()

TRY
// Obtém parâmetros de consulta
sFilter = WebParameter("filter", parameterGet)
nLimit = Val(WebParameter("limit", parameterGet, "100"))
nOffset = Val(WebParameter("offset", parameterGet, "0"))

// Gera JSON com os registros
sJSON = GenerateRecordsJSON(sFilter, nLimit, nOffset)

// Retorna resposta
WebWriteHTTPCode(200)
WebWriteString(sJSON, "application/json")

EXCEPTION
HandleAPIError("Erro ao obter registros", 500)
END
```

END

//——————————————————————————
// GetRecordByID: Retorna registro específico por ID (GET)
//——————————————————————————
PRIVATE PROCEDURE GetRecordByID()
sID is string
sJSON is string

```
SetCORSHeaders()

TRY
sID = WebParameter("id", parameterURL)

IF sID = "" THEN
HandleAPIError("ID não informado", 400)
RETURN
END

// Busca o registro
sJSON = FindRecordByID(sID)

IF sJSON = "" THEN
HandleAPIError("Registro não encontrado", 404)
ELSE
WebWriteHTTPCode(200)
WebWriteString(sJSON, "application/json")
END

EXCEPTION
HandleAPIError("Erro ao buscar registro", 500)
END
```

END

//——————————————————————————
// CreateRecord: Cria novo registro (POST)
//——————————————————————————
PRIVATE PROCEDURE CreateRecord()
sJSONInput is string
sResponse is string
varRecord is Variant

```
SetCORSHeaders()

TRY
// Obtém JSON do body
sJSONInput = WebReadRequestBody()

IF sJSONInput = "" THEN
HandleAPIError("Dados não informados", 400)
RETURN
END

// Parse do JSON
JSONToVariant(varRecord, sJSONInput)

// Valida campos obrigatórios
IF NOT ValidateMandatoryFields(varRecord) THEN
RETURN // Erro já tratado na validação
END

// Cria o registro
sResponse = InsertRecord(varRecord)

WebWriteHTTPCode(201)
WebWriteString(sResponse, "application/json")

EXCEPTION
HandleAPIError("Erro ao criar registro: " + ExceptionInfo(), 500)
END
```

END

//——————————————————————————
// UpdateRecord: Atualiza registro existente (PUT)
//——————————————————————————
PRIVATE PROCEDURE UpdateRecord()
sID is string
sJSONInput is string
sResponse is string
varRecord is Variant

```
SetCORSHeaders()

TRY
sID = WebParameter("id", parameterURL)
sJSONInput = WebReadRequestBody()

IF sID = "" OR sJSONInput = "" THEN
HandleAPIError("ID ou dados não informados", 400)
RETURN
END

JSONToVariant(varRecord, sJSONInput)

// Atualiza o registro
sResponse = ModifyRecord(sID, varRecord)

IF sResponse = "" THEN
HandleAPIError("Registro não encontrado", 404)
ELSE
WebWriteHTTPCode(200)
WebWriteString(sResponse, "application/json")
END

EXCEPTION
HandleAPIError("Erro ao atualizar registro: " + ExceptionInfo(), 500)
END
```

END

//——————————————————————————
// DeleteRecord: Exclui registro (DELETE)
//——————————————————————————
PRIVATE PROCEDURE DeleteRecord()
sID is string

```
SetCORSHeaders()

TRY
sID = WebParameter("id", parameterURL)

IF sID = "" THEN
HandleAPIError("ID não informado", 400)
RETURN
END

IF RemoveRecord(sID) THEN
WebWriteHTTPCode(204) // No Content
ELSE
HandleAPIError("Registro não encontrado", 404)
END

EXCEPTION
HandleAPIError("Erro ao excluir registro: " + ExceptionInfo(), 500)
END
```

END

//==============================================================================
// MÉTODOS DE MANIPULAÇÃO DE DADOS
//==============================================================================

//——————————————————————————
// GenerateRecordsJSON: Gera JSON com registros da tabela
//——————————————————————————
PRIVATE PROCEDURE GenerateRecordsJSON(sFilter is string, nLimit is int, nOffset is int) : string
sJSON is string = “[”
bFirst is boolean = True
sFieldName is string
i is int
nCount is int = 0

```
// Usa indirection para acessar dinamicamente a tabela
{m_sTableName, indFile}..Filter = sFilter

HReadFirst({m_sTableName, indFile})

WHILE NOT HOut({m_sTableName, indFile}) AND nCount < nLimit
IF nCount >= nOffset THEN
IF NOT bFirst THEN
sJSON += ","
END

sJSON += "{"
bFirst = True

// Constrói JSON dinamicamente usando informações dos campos
FOR i = 1 TO ArrayCount(m_stFieldsInfo)
sFieldName = m_stFieldsInfo[i].sName

IF NOT bFirst THEN
sJSON += ","
END

sJSON += """" + sFieldName + """:"
sJSON += FormatFieldValue(sFieldName, {m_sTableName + "." + sFieldName, indFile})

bFirst = False
END

sJSON += "}"
bFirst = False
END

nCount++
HReadNext({m_sTableName, indFile})
END

sJSON += "]"
RESULT sJSON
```

END

//——————————————————————————
// FindRecordByID: Busca registro por ID
//——————————————————————————
PRIVATE PROCEDURE FindRecordByID(sID is string) : string
sJSON is string = “”
sFieldName is string
i is int

```
// Busca usando chave primária com indirection
IF HReadSeekFirst({m_sTableName, indFile}, {m_sPrimaryKey, indFile}, sID) THEN
sJSON = "{"

FOR i = 1 TO ArrayCount(m_stFieldsInfo)
sFieldName = m_stFieldsInfo[i].sName

IF i > 1 THEN
sJSON += ","
END

sJSON += """" + sFieldName + """:"
sJSON += FormatFieldValue(sFieldName, {m_sTableName + "." + sFieldName, indFile})
END

sJSON += "}"
END

RESULT sJSON
```

END

//——————————————————————————
// InsertRecord: Insere novo registro
//——————————————————————————
PRIVATE PROCEDURE InsertRecord(varRecord is Variant) : string
sFieldName is string
i is int

```
// Limpa registro atual
HReset({m_sTableName, indFile})

// Preenche campos usando indirection
FOR i = 1 TO ArrayCount(m_stFieldsInfo)
sFieldName = m_stFieldsInfo[i].sName

IF VariantExists(varRecord, sFieldName) THEN
{m_sTableName + "." + sFieldName, indFile} = varRecord[sFieldName]
END
END

// Insere o registro
IF HAdd({m_sTableName, indFile}) THEN
// Retorna o registro inserido
RESULT FindRecordByID({m_sTableName + "." + m_sPrimaryKey, indFile})
ELSE
ExceptionThrow(2, "Erro ao inserir registro: " + HErrorInfo())
END
```

END

//——————————————————————————
// ModifyRecord: Modifica registro existente
//——————————————————————————
PRIVATE PROCEDURE ModifyRecord(sID is string, varRecord is Variant) : string
sFieldName is string
i is int

```
// Busca o registro
IF NOT HReadSeekFirst({m_sTableName, indFile}, {m_sPrimaryKey, indFile}, sID) THEN
RESULT ""
END

// Atualiza campos
FOR i = 1 TO ArrayCount(m_stFieldsInfo)
sFieldName = m_stFieldsInfo[i].sName

// Não atualiza chave primária
IF sFieldName <> m_sPrimaryKey AND VariantExists(varRecord, sFieldName) THEN
{m_sTableName + "." + sFieldName, indFile} = varRecord[sFieldName]
END
END

// Salva modificações
IF HModify({m_sTableName, indFile}) THEN
RESULT FindRecordByID(sID)
ELSE
ExceptionThrow(3, "Erro ao modificar registro: " + HErrorInfo())
END
```

END

//——————————————————————————
// RemoveRecord: Remove registro
//——————————————————————————
PRIVATE PROCEDURE RemoveRecord(sID is string) : boolean
IF HReadSeekFirst({m_sTableName, indFile}, {m_sPrimaryKey, indFile}, sID) THEN
RESULT HDelete({m_sTableName, indFile})
END

```
RESULT False
```

END

//==============================================================================
// MÉTODOS AUXILIARES
//==============================================================================

//——————————————————————————
// FormatFieldValue: Formata valor do campo para JSON
//——————————————————————————
PRIVATE PROCEDURE FormatFieldValue(sFieldName is string, vValue is Variant) : string
nType is int

```
// Obtém o tipo do campo
nType = HItemType(m_sTableName, sFieldName)

SWITCH nType
CASE hItemText, hItemMemo, hItemBinaryMemo
RESULT """" + Replace(vValue, """", "\""") + """"

CASE hItemDate
IF vValue = "" OR vValue = "00000000" THEN
RESULT "null"
ELSE
RESULT """" + DateToString(vValue, "YYYY-MM-DD") + """"
END

CASE hItemTime
IF vValue = "" OR vValue = "0000" THEN
RESULT "null"
ELSE
RESULT """" + TimeToString(vValue, "HH:MM:SS") + """"
END

CASE hItemDateTime
IF vValue = "" THEN
RESULT "null"
ELSE
RESULT """" + DateTimeToString(vValue, "YYYY-MM-DD HH:MM:SS") + """"
END

CASE hItemBoolean
RESULT IIF(vValue, "true", "false")

CASE hItemNumeric, hItemReal, hItemCurrency
IF vValue = 0 OR vValue = "" THEN
RESULT "0"
ELSE
RESULT Replace(vValue, ",", ".")
END

DEFAULT
IF vValue = Null THEN
RESULT "null"
ELSE
RESULT """" + vValue + """"
END
END
```

END

//——————————————————————————
// ValidateMandatoryFields: Valida campos obrigatórios
//——————————————————————————
PRIVATE PROCEDURE ValidateMandatoryFields(varRecord is Variant) : boolean
i is int
sField is string
sMissingFields is string = “”

```
FOR i = 1 TO ArrayCount(m_stMandatoryFields)
sField = m_stMandatoryFields[i]

IF NOT VariantExists(varRecord, sField) OR varRecord[sField] = "" THEN
IF sMissingFields <> "" THEN
sMissingFields += ", "
END
sMissingFields += sField
END
END

IF sMissingFields <> "" THEN
HandleAPIError("Campos obrigatórios não informados: " + sMissingFields, 400)
RESULT False
END

RESULT True
```

END

//——————————————————————————
// SetCORSHeaders: Define headers CORS
//——————————————————————————
PRIVATE PROCEDURE SetCORSHeaders()
WebWriteHTTPHeader(“Access-Control-Allow-Origin”, “*”)
WebWriteHTTPHeader(“Access-Control-Allow-Methods”, “GET, POST, PUT, DELETE, OPTIONS”)
WebWriteHTTPHeader(“Access-Control-Allow-Headers”, “Content-Type, Authorization”)
END

//——————————————————————————
// HandleCORS: Trata requisições OPTIONS
//——————————————————————————
PRIVATE PROCEDURE HandleCORS()
SetCORSHeaders()
WebWriteHTTPCode(204)
END

//——————————————————————————
// HandleAPIError: Trata erros da API
//——————————————————————————
PRIVATE PROCEDURE HandleAPIError(sMessage is string, nCode is int = 500)
sErrorJSON is string

```
SetCORSHeaders()

sErrorJSON = StringBuild("{\""error\"": {\""message\"": \""%1\"", \""code\"": %2}}", sMessage, nCode)

WebWriteHTTPCode(nCode)
WebWriteString(sErrorJSON, "application/json")
```

END

//==============================================================================
// CLASSE: CRestCRUDClient
// Descrição: Classe cliente para consumir a API REST CRUD
// Autor: Sistema Automático
// Data: 14/08/2025
//==============================================================================

CRestCRUDClient is Class
// Atributos privados
PRIVATE
m_sBaseURL is string
m_sTableName is string
m_sAuthToken is string
m_nTimeout is int = 30000
END

//==============================================================================
// MÉTODOS PÚBLICOS
//==============================================================================

//——————————————————————————
// Constructor: Inicializa cliente da API
//——————————————————————————
PROCEDURE Constructor(sBaseURL is string, sTableName is string, sAuthToken is string = “”)
m_sBaseURL = sBaseURL
m_sTableName = sTableName
m_sAuthToken = sAuthToken

```
// Remove barra final da URL se existir
IF Right(m_sBaseURL, 1) = "/" THEN
m_sBaseURL = Left(m_sBaseURL, Length(m_sBaseURL) - 1)
END
```

END

//——————————————————————————
// GetAll: Obtém todos os registros
//——————————————————————————
PROCEDURE GetAll(sFilter is string = “”, nLimit is int = 100, nOffset is int = 0) : Variant
sURL is string
sResponse is string
varResult is Variant

```
// Constrói URL com parâmetros
sURL = m_sBaseURL + "/" + Lower(m_sTableName)

IF sFilter <> "" OR nLimit <> 100 OR nOffset <> 0 THEN
sURL += "?"

IF sFilter <> "" THEN
sURL += "filter=" + URLEncode(sFilter) + "&"
END

IF nLimit <> 100 THEN
sURL += "limit=" + nLimit + "&"
END

IF nOffset <> 0 THEN
sURL += "offset=" + nOffset + "&"
END

// Remove último &
sURL = Left(sURL, Length(sURL) - 1)
END

// Faz requisição GET
sResponse = ExecuteHTTPRequest("GET", sURL)

// Converte resposta para Variant
JSONToVariant(varResult, sResponse)

RESULT varResult
```

END

//——————————————————————————
// GetByID: Obtém registro por ID
//——————————————————————————
PROCEDURE GetByID(ID is string) : Variant
sURL is string
sResponse is string
varResult is Variant

```
sURL = m_sBaseURL + "/" + Lower(m_sTableName) + "/" + ID

sResponse = ExecuteHTTPRequest("GET", sURL)

JSONToVariant(varResult, sResponse)

RESULT varResult
```

END

//——————————————————————————
// Create: Cria novo registro
//——————————————————————————
PROCEDURE Create(varData is Variant) : Variant
sURL is string
sJSON is string
sResponse is string
varResult is Variant

```
sURL = m_sBaseURL + "/" + Lower(m_sTableName)

// Converte dados para JSON
sJSON = VariantToJSON(varData)

// Faz requisição POST
sResponse = ExecuteHTTPRequest("POST", sURL, sJSON)

JSONToVariant(varResult, sResponse)

RESULT varResult
```

END

//——————————————————————————
// Update: Atualiza registro existente
//——————————————————————————
PROCEDURE Update(ID is string, varData is Variant) : Variant
sURL is string
sJSON is string
sResponse is string
varResult is Variant

```
sURL = m_sBaseURL + "/" + Lower(m_sTableName) + "/" + ID

sJSON = VariantToJSON(varData)

sResponse = ExecuteHTTPRequest("PUT", sURL, sJSON)

JSONToVariant(varResult, sResponse)

RESULT varResult
```

END

//——————————————————————————
// Delete: Exclui registro
//——————————————————————————
PROCEDURE Delete(ID is string) : boolean
sURL is string
sResponse is string

```
sURL = m_sBaseURL + "/" + Lower(m_sTableName) + "/" + ID

TRY
sResponse = ExecuteHTTPRequest("DELETE", sURL)
RESULT True
EXCEPTION
RESULT False
END
```

END

//——————————————————————————
// SetTimeout: Define timeout das requisições
//——————————————————————————
PROCEDURE SetTimeout(nTimeoutMS is int)
m_nTimeout = nTimeoutMS
END

//==============================================================================
// MÉTODOS PRIVADOS
//==============================================================================

//——————————————————————————
// ExecuteHTTPRequest: Executa requisição HTTP
//——————————————————————————
PRIVATE PROCEDURE ExecuteHTTPRequest(sMethod is string, sURL is string, sData is string = “”) : string
cHTTPRequest is httpRequest
cHTTPResponse is httpResponse

```
// Configura requisição
cHTTPRequest.URL = sURL
cHTTPRequest.Method = sMethod
cHTTPRequest.Timeout = m_nTimeout

// Headers padrão
cHTTPRequest.Header["Content-Type"] = "application/json"
cHTTPRequest.Header["Accept"] = "application/json"

// Token de autenticação se informado
IF m_sAuthToken <> "" THEN
cHTTPRequest.Header["Authorization"] = "Bearer " + m_sAuthToken
END

// Dados para POST/PUT
IF sData <> "" THEN
cHTTPRequest.Content = sData
END

// Executa requisição
cHTTPResponse = HTTPSend(cHTTPRequest)

// Verifica resposta
IF cHTTPResponse.StatusCode >= 200 AND cHTTPResponse.StatusCode < 300 THEN
RESULT cHTTPResponse.Content
ELSE
ExceptionThrow(cHTTPResponse.StatusCode, "Erro HTTP " + cHTTPResponse.StatusCode + ": " + cHTTPResponse.Content)
END
```

END

//==============================================================================
// EXEMPLO DE USO DAS CLASSES
//==============================================================================

// Exemplo servidor:
/*
// Cria servidor CRUD para tabela Cliente
crudServer is CRestCRUDServer(“Cliente”)

// Inicia o servidor na porta 8080
crudServer.StartServer(8080, “/api/v1”)
*/

// Exemplo cliente:
/*
// Cria cliente da API
clienteAPI is CRestCRUDClient(“http://localhost:8080/api/v1”, “cliente”)

// Cria novo cliente
novoCliente is Variant
novoCliente.nome = “João Silva”
novoCliente.email = “joao@email.com”
novoCliente.telefone = “11999999999”

clienteCriado is Variant = clienteAPI.Create(novoCliente)
Info(“Cliente criado com ID: “ + clienteCriado.id)

// Busca todos os clientes
todosClientes is Variant = clienteAPI.GetAll()
Info(“Total de clientes: “ + ArrayCount(todosClientes))

// Busca cliente por ID
cliente is Variant = clienteAPI.GetByID(“1”)
Info(“Nome do cliente: “ + cliente.nome)

// Atualiza cliente
dadosAtualizacao is Variant
dadosAtualizacao.telefone = “11888888888”
clienteAtualizado is Variant = clienteAPI.Update(“1”, dadosAtualizacao)

// Exclui cliente
IF clienteAPI.Delete(“1”) THEN
Info(“Cliente excluído com sucesso”)
END
*/

//==============================================================================
// CLASSE: CRestCRUDManager
// Descrição: Gerenciador avançado para múltiplas tabelas e relacionamentos
// Autor: Sistema Automático
// Data: 14/08/2025
//==============================================================================

CRestCRUDManager is Class
PRIVATE
m_stServers is array of stServerInfo
m_stRelationships is array of stTableRelation
m_sConnectionString is string
m_nBasePort is int = 8080
END

// Estruturas para gerenciamento avançado
stServerInfo is Structure
sTableName is string
cServer is CRestCRUDServer
nPort is int
sBasePath is string
bActive is boolean
END

stTableRelation is Structure
sParentTable is string
sChildTable is string
sParentField is string
sChildField is string
sRelationType is string // “one-to-many”, “many-to-one”, “many-to-many”
END

//==============================================================================
// MÉTODOS DO GERENCIADOR AVANÇADO
//==============================================================================

//——————————————————————————
// Constructor: Inicializa gerenciador
//——————————————————————————
PROCEDURE Constructor(sConnection is string = “”, nBasePort is int = 8080)
m_sConnectionString = sConnection
m_nBasePort = nBasePort

```
// Auto-descobre todas as tabelas do projeto
AutoDiscoverTables()
```

END

//——————————————————————————
// AutoDiscoverTables: Descobre automaticamente todas as tabelas
//——————————————————————————
PROCEDURE AutoDiscoverTables()
sTables is string
sTable is string
nPos is int = 1
stServer is stServerInfo

```
// Lista todas as tabelas da análise
sTables = HListFile()

WHILE nPos <= Length(sTables)
sTable = ExtractString(sTables, nPos, CR)

IF sTable <> "" AND Left(sTable, 1) <> "." THEN
// Cria servidor para cada tabela
stServer.sTableName = sTable
stServer.cServer = new CRestCRUDServer(sTable, m_sConnectionString)
stServer.nPort = m_nBasePort + ArrayCount(m_stServers)
stServer.sBasePath = "/api/v1"
stServer.bActive = False

ArrayAdd(m_stServers, stServer)
END

nPos++
END

// Analisa relacionamentos
AnalyzeAllRelationships()
```

END

//——————————————————————————
// StartAllServers: Inicia servidores para todas as tabelas
//——————————————————————————
PROCEDURE StartAllServers() : boolean
i is int
bSuccess is boolean = True

```
FOR i = 1 TO ArrayCount(m_stServers)
IF m_stServers[i].cServer.StartServer(m_stServers[i].nPort, m_stServers[i].sBasePath) THEN
m_stServers[i].bActive = True
Trace("Servidor iniciado: " + m_stServers[i].sTableName + " na porta " + m_stServers[i].nPort)
ELSE
bSuccess = False
Error("Falha ao iniciar servidor para: " + m_stServers[i].sTableName)
END
END

RESULT bSuccess
```

END

//——————————————————————————
// StartServerForTable: Inicia servidor específico para uma tabela
//——————————————————————————
PROCEDURE StartServerForTable(sTableName is string) : boolean
i is int

```
FOR i = 1 TO ArrayCount(m_stServers)
IF Upper(m_stServers[i].sTableName) = Upper(sTableName) THEN
IF m_stServers[i].cServer.StartServer(m_stServers[i].nPort, m_stServers[i].sBasePath) THEN
m_stServers[i].bActive = True
RESULT True
END
END
END

RESULT False
```

END

//——————————————————————————
// GetAPIDocumentation: Gera documentação automática da API
//——————————————————————————
PROCEDURE GetAPIDocumentation() : string
sDoc is string
i is int
j is int

```
sDoc = "# API REST - Documentação Automática" + CR + CR
sDoc += "## Tabelas Disponíveis:" + CR + CR

FOR i = 1 TO ArrayCount(m_stServers)
sDoc += "### " + m_stServers[i].sTableName + CR
sDoc += "**Porta:** " + m_stServers[i].nPort + CR
sDoc += "**Status:** " + IIF(m_stServers[i].bActive, "Ativo", "Inativo") + CR
sDoc += "**Base URL:** http://localhost:" + m_stServers[i].nPort + m_stServers[i].sBasePath + CR + CR

sDoc += "**Endpoints:**" + CR
sDoc += "- GET /" + Lower(m_stServers[i].sTableName) + " - Listar todos" + CR
sDoc += "- GET /" + Lower(m_stServers[i].sTableName) + "/{id} - Obter por ID" + CR
sDoc += "- POST /" + Lower(m_stServers[i].sTableName) + " - Criar novo" + CR
sDoc += "- PUT /" + Lower(m_stServers[i].sTableName) + "/{id} - Atualizar" + CR
sDoc += "- DELETE /" + Lower(m_stServers[i].sTableName) + "/{id} - Excluir" + CR + CR
END

// Adiciona informações sobre relacionamentos
IF ArrayCount(m_stRelationships) > 0 THEN
sDoc += "## Relacionamentos Detectados:" + CR + CR

FOR j = 1 TO ArrayCount(m_stRelationships)
sDoc += "- " + m_stRelationships[j].sParentTable + "." + m_stRelationships[j].sParentField
sDoc += " → " + m_stRelationships[j].sChildTable + "." + m_stRelationships[j].sChildField
sDoc += " (" + m_stRelationships[j].sRelationType + ")" + CR
END
END

RESULT sDoc
```

END

//——————————————————————————
// AnalyzeAllRelationships: Analisa relacionamentos entre tabelas
//——————————————————————————
PRIVATE PROCEDURE AnalyzeAllRelationships()
i is int
j is int
sLinks is string
sLink is string
nLinkPos is int
stRelation is stTableRelation

```
FOR i = 1 TO ArrayCount(m_stServers)
sLinks = HListLink(m_stServers[i].sTableName)
nLinkPos = 1

WHILE nLinkPos <= Length(sLinks)
sLink = ExtractString(sLinks, nLinkPos, CR)

IF sLink <> "" THEN
// Parse do relacionamento
stRelation.sChildTable = m_stServers[i].sTableName
stRelation.sChildField = ExtractString(sLink, 1, TAB)
stRelation.sParentTable = ExtractString(sLink, 2, TAB)
stRelation.sParentField = ExtractString(sLink, 3, TAB)
stRelation.sRelationType = "many-to-one"

ArrayAdd(m_stRelationships, stRelation)
END

nLinkPos++
END
END
```

END

//==============================================================================
// CLASSE: CRestCRUDHelper
// Descrição: Utilitários e helpers para facilitar desenvolvimento
// Autor: Sistema Automático
// Data: 14/08/2025
//==============================================================================

CRestCRUDHelper is Class

END

//==============================================================================
// MÉTODOS ESTÁTICOS UTILITÁRIOS
//==============================================================================

//——————————————————————————
// GenerateClientCode: Gera código cliente automaticamente
//——————————————————————————
PROCEDURE STATIC GenerateClientCode(sTableName is string, sBaseURL is string) : string
sCode is string
sClassName is string

```
sClassName = "C" + sTableName + "API"

sCode = "// Classe cliente gerada automaticamente para " + sTableName + CR
sCode += sClassName + " is Class" + CR
sCode += " PRIVATE" + CR
sCode += " m_cClient is CRestCRUDClient" + CR
sCode += "END" + CR + CR

sCode += "PROCEDURE Constructor()" + CR
sCode += " m_cClient = new CRestCRUDClient(""" + sBaseURL + """, """ + Lower(sTableName) + """)" + CR
sCode += "END" + CR + CR

// Métodos específicos para a tabela
sCode += "PROCEDURE List" + sTableName + "(sFilter is string = """") : Variant" + CR
sCode += " RESULT m_cClient.GetAll(sFilter)" + CR
sCode += "END" + CR + CR

sCode += "PROCEDURE Get" + sTableName + "(ID is string) : Variant" + CR
sCode += " RESULT m_cClient.GetByID(ID)" + CR
sCode += "END" + CR + CR

sCode += "PROCEDURE Create" + sTableName + "(var" + sTableName + " is Variant) : Variant" + CR
sCode += " RESULT m_cClient.Create(var" + sTableName + ")" + CR
sCode += "END" + CR + CR

sCode += "PROCEDURE Update" + sTableName + "(ID is string, var" + sTableName + " is Variant) : Variant" + CR
sCode += " RESULT m_cClient.Update(ID, var" + sTableName + ")" + CR
sCode += "END" + CR + CR

sCode += "PROCEDURE Delete" + sTableName + "(ID is string) : boolean" + CR
sCode += " RESULT m_cClient.Delete(ID)" + CR
sCode += "END" + CR

RESULT sCode
```

END

//——————————————————————————
// GenerateSwaggerDoc: Gera documentação Swagger/OpenAPI
//——————————————————————————
PROCEDURE STATIC GenerateSwaggerDoc(sTableName is string, stFields is array of stFieldInfo) : string
sSwagger is string
i is int

```
sSwagger = "{" + CR
sSwagger += " ""openapi"": ""3.0.0""," + CR
sSwagger += " ""info"": {" + CR
sSwagger += " ""title"": """ + sTableName + " API""," + CR
sSwagger += " ""version"": ""1.0.0""," + CR
sSwagger += " ""description"": ""API REST para tabela " + sTableName + """" + CR
sSwagger += " }," + CR
sSwagger += " ""paths"": {" + CR

// GET All
sSwagger += " ""/" + Lower(sTableName) + """: {" + CR
sSwagger += " ""get"": {" + CR
sSwagger += " ""summary"": ""Listar todos os registros""," + CR
sSwagger += " ""parameters"": [" + CR
sSwagger += " {""name"": ""filter"", ""in"": ""query"", ""schema"": {""type"": ""string""}}," + CR
sSwagger += " {""name"": ""limit"", ""in"": ""query"", ""schema"": {""type"": ""integer""}}," + CR
sSwagger += " {""name"": ""offset"", ""in"": ""query"", ""schema"": {""type"": ""integer""}}" + CR
sSwagger += " ]," + CR
sSwagger += " ""responses"": {" + CR
sSwagger += " ""200"": {""description"": ""Lista de registros""}" + CR
sSwagger += " }" + CR
sSwagger += " }," + CR

// POST Create
sSwagger += " ""post"": {" + CR
sSwagger += " ""summary"": ""Criar novo registro""," + CR
sSwagger += " ""requestBody"": {" + CR
sSwagger += " ""content"": {" + CR
sSwagger += " ""application/json"": {" + CR
sSwagger += " ""schema"": " + GenerateJSONSchema(stFields) + CR
sSwagger += " }" + CR
sSwagger += " }" + CR
sSwagger += " }," + CR
sSwagger += " ""responses"": {" + CR
sSwagger += " ""201"": {""description"": ""Registro criado""}" + CR
sSwagger += " }" + CR
sSwagger += " }" + CR
sSwagger += " }" + CR

sSwagger += " }" + CR
sSwagger += "}" + CR

RESULT sSwagger
```

END

//——————————————————————————
// GenerateJSONSchema: Gera schema JSON para documentação
//——————————————————————————
PROCEDURE STATIC GenerateJSONSchema(stFields is array of stFieldInfo) : string
sSchema is string
i is int

```
sSchema = "{" + CR + " ""type"": ""object""," + CR + " ""properties"": {" + CR

FOR i = 1 TO ArrayCount(stFields)
IF i > 1 THEN
sSchema += "," + CR
END

sSchema += " """ + stFields[i].sName + """: {" + CR
sSchema += " ""type"": """ + GetJSONType(stFields[i].nType) + """" + CR

IF stFields[i].bMandatory THEN
sSchema += "," + CR + " ""required"": true" + CR
END

sSchema += " }"
END

sSchema += CR + " }" + CR + "}"

RESULT sSchema
```

END

//——————————————————————————
// GetJSONType: Converte tipo HFSQL para tipo JSON
//——————————————————————————
PROCEDURE STATIC GetJSONType(nHFSQLType is int) : string
SWITCH nHFSQLType
CASE hItemText, hItemMemo, hItemBinaryMemo
RESULT “string”
CASE hItemNumeric, hItemReal, hItemCurrency
RESULT “number”
CASE hItemBoolean
RESULT “boolean”
CASE hItemDate, hItemTime, hItemDateTime
RESULT “string”
DEFAULT
RESULT “string”
END
END

//——————————————————————————
// ValidateJSON: Valida estrutura JSON contra schema da tabela
//——————————————————————————
PROCEDURE STATIC ValidateJSON(sJSON is string, stFields is array of stFieldInfo) : boolean
varData is Variant
i is int

```
TRY
JSONToVariant(varData, sJSON)

// Valida cada campo obrigatório
FOR i = 1 TO ArrayCount(stFields)
IF stFields[i].bMandatory THEN
IF NOT VariantExists(varData, stFields[i].sName) THEN
RESULT False
END
END
END

RESULT True

EXCEPTION
RESULT False
END
```

END

//==============================================================================
// EXEMPLO COMPLETO DE USO DO SISTEMA
//==============================================================================

/*
//——————————————————————————
// EXEMPLO 1: Uso básico - Uma tabela
//——————————————————————————

// Servidor
serverCliente is CRestCRUDServer(“Cliente”)
serverCliente.StartServer(8080)

// Cliente
clienteAPI is CRestCRUDClient(“http://localhost:8080/api/v1”, “cliente”)

// Operações
novoCliente is Variant
novoCliente.nome = “Maria Silva”
novoCliente.email = “maria@teste.com”
resultado is Variant = clienteAPI.Create(novoCliente)

//——————————————————————————
// EXEMPLO 2: Uso avançado - Múltiplas tabelas
//——————————————————————————

// Gerenciador para todas as tabelas
manager is CRestCRUDManager()

// Inicia servidores para todas as tabelas descobertas
manager.StartAllServers()

// Gera documentação
sDocumentacao is string = manager.GetAPIDocumentation()
fSaveText(“c:\temp\api_docs.md”, sDocumentacao)

//——————————————————————————
// EXEMPLO 3: Geração de código cliente
//——————————————————————————

// Gera código cliente específico para tabela Cliente
sCodigoCliente is string = CRestCRUDHelper.GenerateClientCode(“Cliente”, “http://localhost:8080/api/v1”)
fSaveText(“c:\temp\CClienteAPI.wl”, sCodigoCliente)

// Gera documentação Swagger
stCampos is array of stFieldInfo
// … preenche campos …
sSwagger is string = CRestCRUDHelper.GenerateSwaggerDoc(“Cliente”, stCampos)
fSaveText(“c:\temp\swagger.json”, sSwagger)

//——————————————————————————
// EXEMPLO 4: API com Query SQL personalizada
//——————————————————————————

sQueryPersonalizada is string = “SELECT c.nome, c.email, p.descricao as perfil “ + …
“FROM Cliente c INNER JOIN Perfil p ON c.id_perfil = p.id “ + …
“WHERE c.ativo = 1”

serverQuery is CRestCRUDServer(sQueryPersonalizada)
serverQuery.StartServer(8081, “/api/v1”)

//——————————————————————————
// EXEMPLO 5: Cliente com autenticação
//——————————————————————————

clienteSeguro is CRestCRUDClient(“https://api.empresa.com/v1”, “cliente”, “seu_token_jwt_aqui”)
clienteSeguro.SetTimeout(60000) // 60 segundos

dados is Variant = clienteSeguro.GetAll(“ativo=1”, 50, 0)
*/


Principais vantagens do sistema completo:
✅ Zero configuração
• Apenas informe o nome da tabela e o sistema gera automaticamente toda a API REST
• Análise automática da estrutura usando comandos H (HListFile, HListItem, HItemType, etc.)
✅ Uso extensivo de indirection
• {m_sTableName, indFile} para acesso dinâmico às tabelas
• {m_sTableName + "." + sFieldName, indFile} para campos
• Máximo reaproveitamento de código - uma classe serve para qualquer tabela
✅ Gerenciamento inteligente
• CRestCRUDManager: Descobre automaticamente todas as tabelas do projeto
• Inicia múltiplos servidores simultaneamente
• Gera documentação automática completa
✅ Utilitários avançados
• CRestCRUDHelper: Gera código cliente automaticamente
• Documentação Swagger/OpenAPI automática
• Validação de JSON contra schema da tabela
✅ Recursos especiais
• Campos obrigatórios: Validação automática antes de inserir
• Chaves primárias/estrangeiras: Detecta e usa automaticamente
• Relacionamentos: Analisa links entre tabelas
• Queries SQL: Suporte a SELECT personalizados
• CORS: Configurado para integração web
• Autenticação: Suporte a Bearer tokens
✅ Facilidade de uso
Para criar API de uma tabela:

servidor is CRestCRUDServer("MinhaTabela")
servidor.StartServer(8080)


Para consumir a API:
cliente is CRestCRUDClient("http://localhost:8080/api/v1", "minhatabela")
dados is Variant = cliente.GetAll()


Para múltiplas tabelas:
manager is CRestCRUDManager()
manager.StartAllServers() // Inicia API para TODAS as tabelas


📚 Documentação automática
O sistema gera automaticamente:
• Documentação Markdown com todos os endpoints
• Especificação Swagger/OpenAPI
• Código cliente específico para cada tabela
• Lista de relacionamentos detectados
Este sistema permite criar APIs REST profissionais em WLanguage com zero configuração manual, aproveitando ao máximo a indirection e os comandos H para máxima flexibilidade e reaproveitamento de código!​​​​​​​​​​​​​​​​

# Guia Completo de Instalação e Uso - CRUD REST API WLanguage

## 📋 Pré-requisitos

- **WinDev 28** ou superior (funciona também no WebDev/WinDev Mobile)
- **Análise com pelo menos uma tabela** criada
- **Projeto configurado** com conexão à base de dados
- **Porta disponível** para o servidor (padrão: 8080)

## 📦 Instalação

### Passo 1: Adicionar as Classes ao Projeto

1. **Crie um novo elemento** no seu projeto: `Ctrl + N`
1. **Selecione “Classe”**
1. **Nome**: `CRestCRUDServer`
1. **Cole o código da classe CRestCRUDServer** do artifact anterior
1. **Repita para**: `CRestCRUDClient`, `CRestCRUDManager`, `CRestCRUDHelper`

### Passo 2: Verificar Dependências

Certifique-se que seu projeto tem:

- **Componente Web** ativado (para WebServer)
- **Componente HTTP** ativado (para HTTPSend)
- **Acesso à análise** configurado

## 🚀 Exemplos Práticos de Uso

### Exemplo 1: API Simples para Uma Tabela

Vamos criar uma API para a tabela `CLIENTE`:

```wlanguage
//==============================================================================
// ARQUIVO: ServerCliente.wl
// Descrição: Exemplo básico - Servidor para tabela Cliente
//==============================================================================

PROCEDURE Main()
// Cria e inicia servidor para tabela Cliente
serverCliente is CRestCRUDServer("Cliente")

// Inicia servidor na porta 8080
IF serverCliente.StartServer(8080, "/api/v1") THEN
Info("✅ Servidor Cliente iniciado com sucesso!" + CR + CR + ...
"Endpoints disponíveis:" + CR + ...
"• GET http://localhost:8080/api/v1/cliente" + CR + ...
"• POST http://localhost:8080/api/v1/cliente" + CR + ...
"• PUT http://localhost:8080/api/v1/cliente/{id}" + CR + ...
"• DELETE http://localhost:8080/api/v1/cliente/{id}")

// Mantém servidor ativo
LOOP
Multitask(100)
END
ELSE
Error("❌ Falha ao iniciar servidor!")
END
END
```

### Exemplo 2: Cliente Consumindo a API

```wlanguage
//==============================================================================
// ARQUIVO: ClienteAPI.wl
// Descrição: Exemplo de cliente consumindo API Cliente
//==============================================================================

PROCEDURE TestarAPICliente()
// Cria cliente da API
clienteAPI is CRestCRUDClient("http://localhost:8080/api/v1", "cliente")

// Define timeout de 30 segundos
clienteAPI.SetTimeout(30000)

TRY
//----------------------------------------------------------------------
// 1. CRIAR NOVO CLIENTE
//----------------------------------------------------------------------
Info("🔄 Criando novo cliente...")

novoCliente is Variant
novoCliente.nome = "João Silva"
novoCliente.email = "joao.silva@email.com"
novoCliente.telefone = "11999887766"
novoCliente.cidade = "São Paulo"
novoCliente.ativo = True

clienteCriado is Variant = clienteAPI.Create(novoCliente)

sIDCriado is string = clienteCriado.id
Info("✅ Cliente criado com sucesso!" + CR + ...
"ID: " + sIDCriado + CR + ...
"Nome: " + clienteCriado.nome)

//----------------------------------------------------------------------
// 2. BUSCAR CLIENTE POR ID
//----------------------------------------------------------------------
Info("🔄 Buscando cliente por ID...")

clienteEncontrado is Variant = clienteAPI.GetByID(sIDCriado)
Info("✅ Cliente encontrado:" + CR + ...
"Nome: " + clienteEncontrado.nome + CR + ...
"Email: " + clienteEncontrado.email + CR + ...
"Telefone: " + clienteEncontrado.telefone)

//----------------------------------------------------------------------
// 3. ATUALIZAR CLIENTE
//----------------------------------------------------------------------
Info("🔄 Atualizando cliente...")

dadosAtualizacao is Variant
dadosAtualizacao.telefone = "11888777666"
dadosAtualizacao.observacoes = "Cliente VIP - Atualizado via API"

clienteAtualizado is Variant = clienteAPI.Update(sIDCriado, dadosAtualizacao)
Info("✅ Cliente atualizado!" + CR + ...
"Novo telefone: " + clienteAtualizado.telefone + CR + ...
"Observações: " + clienteAtualizado.observacoes)

//----------------------------------------------------------------------
// 4. LISTAR TODOS OS CLIENTES
//----------------------------------------------------------------------
Info("🔄 Listando todos os clientes...")

todosClientes is Variant = clienteAPI.GetAll("", 10, 0)

sListaClientes is string = "📋 Lista de Clientes:" + CR + CR
FOR i = 1 TO ArrayCount(todosClientes)
sListaClientes += StringBuild("• ID: %1 - Nome: %2 - Email: %3" + CR,
todosClientes[i].id,
todosClientes[i].nome,
todosClientes[i].email)
END

Info(sListaClientes)

//----------------------------------------------------------------------
// 5. BUSCAR COM FILTRO
//----------------------------------------------------------------------
Info("🔄 Buscando clientes ativos...")

clientesAtivos is Variant = clienteAPI.GetAll("ativo=1", 20, 0)
Info("✅ Encontrados " + ArrayCount(clientesAtivos) + " clientes ativos")

//----------------------------------------------------------------------
// 6. EXCLUIR CLIENTE (OPCIONAL)
//----------------------------------------------------------------------
nResposta is int = YesNo("❓ Deseja excluir o cliente criado no teste?")

IF nResposta = Yes THEN
IF clienteAPI.Delete(sIDCriado) THEN
Info("✅ Cliente excluído com sucesso!")
ELSE
Error("❌ Erro ao excluir cliente")
END
END

EXCEPTION
Error("❌ Erro durante teste da API: " + CR + ExceptionInfo())
END
END
```

### Exemplo 3: Servidor Multi-Tabelas Automático

```wlanguage
//==============================================================================
// ARQUIVO: ServidorCompleto.wl
// Descrição: Servidor automático para TODAS as tabelas do projeto
//==============================================================================

PROCEDURE IniciarServidorCompleto()
// Cria gerenciador para todas as tabelas
manager is CRestCRUDManager()

Info("🔄 Iniciando descoberta automática de tabelas...")

// Gera e exibe documentação antes de iniciar
sDocumentacao is string = manager.GetAPIDocumentation()

// Salva documentação em arquivo
IF fSaveText(fExeDir() + "\API_Documentation.md", sDocumentacao) THEN
Info("📄 Documentação salva em: " + fExeDir() + "\API_Documentation.md")
END

// Exibe prévia da documentação
Info("📋 Tabelas descobertas:" + CR + CR + Left(sDocumentacao, 500) + "...")

// Pergunta se deseja continuar
IF YesNo("🚀 Iniciar servidores para todas as tabelas?") = Yes THEN

Info("🔄 Iniciando servidores...")

IF manager.StartAllServers() THEN
Info("✅ Todos os servidores iniciados com sucesso!" + CR + CR + ...
"📖 Consulte o arquivo API_Documentation.md para detalhes completos" + CR + CR + ...
"🌐 Exemplo de acesso:" + CR + ...
"http://localhost:8080/api/v1/cliente" + CR + ...
"http://localhost:8081/api/v1/produto" + CR + ...
"http://localhost:8082/api/v1/pedido")

// Mantém servidores ativos
Info("⚡ Servidores rodando... Pressione OK para parar.")

ELSE
Error("❌ Falha ao iniciar alguns servidores!")
END
END
END
```

### Exemplo 4: Cliente com Tratamento de Erros Avançado

```wlanguage
//==============================================================================
// ARQUIVO: ClienteAvancado.wl
// Descrição: Cliente com tratamento completo de erros e logging
//==============================================================================

PROCEDURE TestarAPIAvancada()
sLog is string = ""

// Função interna para logging
INTERNAL PROCEDURE Log(sMessage)
sTimeStamp is string = DateTimeToString(Now(), "DD/MM/YYYY HH:MM:SS")
sLog += "[" + sTimeStamp + "] " + sMessage + CR
Trace(sMessage)
END

Log("🚀 Iniciando teste avançado da API")

// Cria cliente com configurações personalizadas
produtoAPI is CRestCRUDClient("http://localhost:8081/api/v1", "produto")
produtoAPI.SetTimeout(60000) // 60 segundos

TRY
//----------------------------------------------------------------------
// Teste de conectividade
//----------------------------------------------------------------------
Log("🔗 Testando conectividade...")

testeProdutos is Variant = produtoAPI.GetAll("", 1, 0)
Log("✅ Conectividade OK - API respondendo")

//----------------------------------------------------------------------
// Operações em lote
//----------------------------------------------------------------------
Log("📦 Criando produtos em lote...")

arrayProdutos is array of Variant

FOR i = 1 TO 5
novoProduto is Variant
novoProduto.nome = "Produto Teste " + i
novoProduto.preco = 10.50 * i
novoProduto.categoria = "Eletrônicos"
novoProduto.estoque = 100 + i * 10
novoProduto.ativo = True

ArrayAdd(arrayProdutos, novoProduto)
END

arrayIDsCriados is array of string

FOR i = 1 TO ArrayCount(arrayProdutos)
produtoCriado is Variant = produtoAPI.Create(arrayProdutos[i])
ArrayAdd(arrayIDsCriados, produtoCriado.id)
Log("✅ Produto criado - ID: " + produtoCriado.id + " - Nome: " + produtoCriado.nome)
END

//----------------------------------------------------------------------
// Teste de paginação
//----------------------------------------------------------------------
Log("📄 Testando paginação...")

nPagina is int = 1
nTamanhoPagina is int = 3
nTotalRegistros is int = 0

LOOP
produtosPagina is Variant = produtoAPI.GetAll("", nTamanhoPagina, (nPagina - 1) * nTamanhoPagina)

nRegistrosPagina is int = ArrayCount(produtosPagina)

IF nRegistrosPagina = 0 THEN
BREAK
END

nTotalRegistros += nRegistrosPagina
Log(StringBuild("📄 Página %1: %2 registros", nPagina, nRegistrosPagina))

nPagina++

// Evita loop infinito
IF nPagina > 10 THEN
BREAK
END
END

Log("📊 Total de registros encontrados: " + nTotalRegistros)

//----------------------------------------------------------------------
// Atualização em lote
//----------------------------------------------------------------------
Log("🔄 Atualizando produtos criados...")

FOR i = 1 TO ArrayCount(arrayIDsCriados)
dadosAtualizacao is Variant
dadosAtualizacao.preco = dadosAtualizacao.preco * 1.1 // Aumento de 10%
dadosAtualizacao.observacoes = "Preço atualizado automaticamente"

produtoAtualizado is Variant = produtoAPI.Update(arrayIDsCriados[i], dadosAtualizacao)
Log("💰 Preço atualizado - ID: " + arrayIDsCriados[i] + " - Novo preço: " + produtoAtualizado.preco)
END

//----------------------------------------------------------------------
// Limpeza (exclusão dos produtos de teste)
//----------------------------------------------------------------------
IF YesNo("🗑️ Excluir produtos de teste criados?") = Yes THEN
Log("🗑️ Excluindo produtos de teste...")

FOR i = 1 TO ArrayCount(arrayIDsCriados)
IF produtoAPI.Delete(arrayIDsCriados[i]) THEN
Log("✅ Produto excluído - ID: " + arrayIDsCriados[i])
ELSE
Log("❌ Erro ao excluir produto - ID: " + arrayIDsCriados[i])
END
END
END

EXCEPTION
Log("❌ ERRO: " + ExceptionInfo())
END

// Salva log completo
sLogFile is string = fExeDir() + "\API_Test_" + DateToString(Today(), "YYYYMMDD_HHMMSS") + ".log"

IF fSaveText(sLogFile, sLog) THEN
Log("📄 Log salvo em: " + sLogFile)
Info("✅ Teste concluído!" + CR + CR + "📄 Log detalhado salvo em:" + CR + sLogFile)
END
END
```

## 🔧 Configurações Avançadas

### Personalizar Portas e Caminhos

```wlanguage
// Servidor em porta específica
servidor is CRestCRUDServer("MinhaTabela")
servidor.StartServer(9090, "/minha-api/v2")

// Cliente para API personalizada
cliente is CRestCRUDClient("http://localhost:9090/minha-api/v2", "minhatabela")
```

### API com Autenticação

```wlanguage
// Cliente com token JWT
clienteSeguro is CRestCRUDClient("https://api.empresa.com/v1", "cliente", "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...")

// Operações autenticadas
dados is Variant = clienteSeguro.GetAll()
```

### Servidor para Query SQL Personalizada

```wlanguage
sQuery is string = "SELECT c.nome, c.email, p.descricao as perfil " + ...
"FROM Cliente c INNER JOIN Perfil p ON c.id_perfil = p.id " + ...
"WHERE c.ativo = 1"

serverQuery is CRestCRUDServer(sQuery)
serverQuery.StartServer(8085, "/api/v1")
```

## 🐛 Solução de Problemas

### Erro: “Tabela não encontrada”

```wlanguage
// Verifique se a tabela existe na análise
sTables is string = HListFile()
IF Position(sTables, "MinhaTabela", firstRank, IgnoreCase) = 0 THEN
Error("Tabela não encontrada na análise!")
END
```

### Erro: “Porta já em uso”

```wlanguage
// Teste portas diferentes
FOR nPorta = 8080 TO 8090
IF servidor.StartServer(nPorta) THEN
Info("Servidor iniciado na porta: " + nPorta)
BREAK
END
END
```

### Erro: “Conexão recusada”

```wlanguage
// Verifique se o servidor está rodando
cHTTP is httpRequest
cHTTP.URL = "http://localhost:8080/api/v1/cliente"
cHTTP.Method = "GET"

cResponse is httpResponse = HTTPSend(cHTTP)
IF cResponse.StatusCode <> 200 THEN
Error("Servidor não está respondendo!")
END
```

## 📚 Próximos Passos

1. **Teste os exemplos** acima em seu projeto
1. **Adapte as classes** para suas necessidades específicas
1. **Explore a documentação automática** gerada
1. **Implemente autenticação** se necessário
1. **Configure HTTPS** para produção
1. **Adicione logs** e monitoramento

## 🎯 Dicas de Performance

- Use **filtros** para limitar resultados: `GetAll("ativo=1", 50, 0)`
- Configure **timeout** adequado: `SetTimeout(30000)`
- Implemente **cache** no lado cliente se necessário
- Use **paginação** para grandes volumes de dados
- Monitore **uso de memória** em servidores com muitas tabelas

Criei um guia completo e prático com exemplos reais de instalação e uso do sistema CRUD REST API em WLanguage!
🎯 Destaques do Guia:
📦 Instalação Simples
• Passo a passo para adicionar as classes ao projeto
• Verificação de dependências necessárias
• Configuração mínima requerida
🚀 Exemplos Práticos Escaláveis
1. API Simples - Servidor básico para uma tabela
2. Cliente Completo - Todos os métodos CRUD com tratamento de erros
3. Multi-Tabelas - Servidor automático para todas as tabelas
4. Cliente Avançado - Operações em lote, paginação, logging
🔧 Configurações Flexíveis
• Portas personalizadas
• Caminhos de API customizados
• Autenticação JWT
• Queries SQL personalizadas
🐛 Solução de Problemas
• Erros comuns e suas soluções
• Verificações de conectividade
• Testes de porta e configuração
💡 Casos de Uso Reais:
✅ Para desenvolvimento rápido:
servidor is CRestCRUDServer("Cliente")
servidor.StartServer(8080)



✅ Para produção empresarial:
manager is CRestCRUDManager()
manager.StartAllServers() // API para TODAS as tabelas!



✅ Para integração externa:
clienteAPI is CRestCRUDClient("https://api.empresa.com/v1", "cliente", "token_jwt")
dados is Variant = clienteAPI.GetAll()



O sistema permite desde protótipos rápidos até soluções enterprise completas, aproveitando ao máximo a análise do WinDev e indirection para máximo reaproveitamento de código!

Exemplo de pedido e itens
//==============================================================================
// EXEMPLO COMPLETO: SISTEMA DE PEDIDOS E ITENS
// Descrição: Demonstra uso prático com relacionamentos mestre-detalhe
// Tabelas: PEDIDO, ITEM_PEDIDO, CLIENTE, PRODUTO
// Autor: Sistema Automático
// Data: 14/08/2025
//==============================================================================

//==============================================================================
// ESTRUTURA DAS TABELAS (Para referência)
//==============================================================================
/*
CLIENTE:

- id (Numeric, Primary Key, Auto Increment)
- nome (Text 100, Mandatory)
- email (Text 150, Mandatory, Unique)
- telefone (Text 20)
- endereco (Text 200)
- cidade (Text 50)
- ativo (Boolean, Default True)

PRODUTO:

- id (Numeric, Primary Key, Auto Increment)
- nome (Text 100, Mandatory)
- descricao (Text 500)
- preco (Currency, Mandatory)
- estoque (Numeric, Default 0)
- categoria (Text 50)
- ativo (Boolean, Default True)

PEDIDO:

- id (Numeric, Primary Key, Auto Increment)
- id_cliente (Numeric, Foreign Key -> CLIENTE.id, Mandatory)
- data_pedido (Date, Mandatory, Default Today)
- hora_pedido (Time, Default Now)
- status (Text 20, Default “PENDENTE”) // PENDENTE, CONFIRMADO, ENVIADO, ENTREGUE, CANCELADO
- valor_total (Currency, Default 0)
- observacoes (Text 500)
- data_entrega (Date)

ITEM_PEDIDO:

- id (Numeric, Primary Key, Auto Increment)
- id_pedido (Numeric, Foreign Key -> PEDIDO.id, Mandatory)
- id_produto (Numeric, Foreign Key -> PRODUTO.id, Mandatory)
- quantidade (Numeric, Mandatory, Default 1)
- preco_unitario (Currency, Mandatory)
- preco_total (Currency, Calculated: quantidade * preco_unitario)
- observacoes (Text 200)
*/

//==============================================================================
// SERVIDOR: INICIALIZANDO APIS PARA TODAS AS TABELAS
//==============================================================================

PROCEDURE IniciarServidorPedidos()
// Gerenciador para todas as tabelas do sistema
manager is CRestCRUDManager()

```
Info("🚀 Iniciando Sistema de Pedidos - APIs REST")

// Exibe documentação das tabelas encontradas
sDoc is string = manager.GetAPIDocumentation()
Trace("📋 Tabelas do sistema:" + CR + sDoc)

// Inicia servidores para todas as tabelas
IF manager.StartAllServers() THEN
Info("✅ Servidor de Pedidos iniciado com sucesso!" + CR + CR + ...
"🌐 APIs disponíveis:" + CR + ...
"• CLIENTE: http://localhost:8080/api/v1/cliente" + CR + ...
"• PRODUTO: http://localhost:8081/api/v1/produto" + CR + ...
"• PEDIDO: http://localhost:8082/api/v1/pedido" + CR + ...
"• ITEM_PEDIDO: http://localhost:8083/api/v1/item_pedido" + CR + CR + ...
"📖 Consulte API_Documentation.md para detalhes")

// Mantém servidores ativos
Info("⚡ Servidores rodando... Pressione OK para parar.")
ELSE
Error("❌ Falha ao iniciar servidores!")
END
```

END

//==============================================================================
// CLASSE: CPedidoManager
// Descrição: Gerenciador de alto nível para operações de pedidos
//==============================================================================

CPedidoManager is Class
PRIVATE
m_cClienteAPI is CRestCRUDClient
m_cProdutoAPI is CRestCRUDClient
m_cPedidoAPI is CRestCRUDClient
m_cItemPedidoAPI is CRestCRUDClient
m_sBaseURL is string
END

//——————————————————————————
// Constructor: Inicializa todos os clientes API
//——————————————————————————
PROCEDURE Constructor(sBaseURL is string = “http://localhost”)
m_sBaseURL = sBaseURL

```
// Inicializa clientes para cada tabela
m_cClienteAPI = new CRestCRUDClient(sBaseURL + ":8080/api/v1", "cliente")
m_cProdutoAPI = new CRestCRUDClient(sBaseURL + ":8081/api/v1", "produto")
m_cPedidoAPI = new CRestCRUDClient(sBaseURL + ":8082/api/v1", "pedido")
m_cItemPedidoAPI = new CRestCRUDClient(sBaseURL + ":8083/api/v1", "item_pedido")

// Timeout de 60 segundos para operações complexas
m_cClienteAPI.SetTimeout(60000)
m_cProdutoAPI.SetTimeout(60000)
m_cPedidoAPI.SetTimeout(60000)
m_cItemPedidoAPI.SetTimeout(60000)
```

END

//==============================================================================
// MÉTODOS DE NEGÓCIO - PEDIDOS COMPLETOS
//==============================================================================

//——————————————————————————
// CriarPedidoCompleto: Cria pedido com itens em uma transação
//——————————————————————————
PROCEDURE CriarPedidoCompleto(varPedidoCompleto is Variant) : Variant
varPedidoCriado is Variant
varResult is Variant
arrayItens is array of Variant
nValorTotal is currency = 0
i is int

```
TRY
Trace("🛒 Iniciando criação de pedido completo...")

// 1. Valida se cliente existe
varCliente is Variant = m_cClienteAPI.GetByID(varPedidoCompleto.id_cliente)
IF VariantToJSON(varCliente) = "null" THEN
ExceptionThrow(1, "Cliente não encontrado: " + varPedidoCompleto.id_cliente)
END

Trace("✅ Cliente validado: " + varCliente.nome)

// 2. Valida produtos e calcula valor total
IF VariantExists(varPedidoCompleto, "itens") THEN
arrayItens = varPedidoCompleto.itens

FOR i = 1 TO ArrayCount(arrayItens)
// Valida produto
varProduto is Variant = m_cProdutoAPI.GetByID(arrayItens[i].id_produto)
IF VariantToJSON(varProduto) = "null" THEN
ExceptionThrow(2, "Produto não encontrado: " + arrayItens[i].id_produto)
END

// Valida estoque
IF varProduto.estoque < arrayItens[i].quantidade THEN
ExceptionThrow(3, "Estoque insuficiente para produto: " + varProduto.nome +
" (Disponível: " + varProduto.estoque +
", Solicitado: " + arrayItens[i].quantidade + ")")
END

// Usa preço atual do produto se não informado
IF NOT VariantExists(arrayItens[i], "preco_unitario") THEN
arrayItens[i].preco_unitario = varProduto.preco
END

// Calcula preço total do item
arrayItens[i].preco_total = arrayItens[i].quantidade * arrayItens[i].preco_unitario
nValorTotal += arrayItens[i].preco_total

Trace("✅ Item validado: " + varProduto.nome +
" (Qtd: " + arrayItens[i].quantidade +
", Valor: " + arrayItens[i].preco_total + ")")
END
END

// 3. Cria o pedido (cabeçalho)
varNovoPedido is Variant
varNovoPedido.id_cliente = varPedidoCompleto.id_cliente
varNovoPedido.data_pedido = IIF(VariantExists(varPedidoCompleto, "data_pedido"),
varPedidoCompleto.data_pedido,
DateToString(Today(), "YYYY-MM-DD"))
varNovoPedido.hora_pedido = IIF(VariantExists(varPedidoCompleto, "hora_pedido"),
varPedidoCompleto.hora_pedido,
TimeToString(Now(), "HH:MM:SS"))
varNovoPedido.status = IIF(VariantExists(varPedidoCompleto, "status"),
varPedidoCompleto.status,
"PENDENTE")
varNovoPedido.valor_total = nValorTotal
varNovoPedido.observacoes = IIF(VariantExists(varPedidoCompleto, "observacoes"),
varPedidoCompleto.observacoes,
"")

varPedidoCriado = m_cPedidoAPI.Create(varNovoPedido)

Trace("✅ Pedido criado - ID: " + varPedidoCriado.id + ", Valor: " + varPedidoCriado.valor_total)

// 4. Cria os itens do pedido
arrayItensCriados is array of Variant

FOR i = 1 TO ArrayCount(arrayItens)
varNovoItem is Variant
varNovoItem.id_pedido = varPedidoCriado.id
varNovoItem.id_produto = arrayItens[i].id_produto
varNovoItem.quantidade = arrayItens[i].quantidade
varNovoItem.preco_unitario = arrayItens[i].preco_unitario
varNovoItem.preco_total = arrayItens[i].preco_total
varNovoItem.observacoes = IIF(VariantExists(arrayItens[i], "observacoes"),
arrayItens[i].observacoes,
"")

varItemCriado is Variant = m_cItemPedidoAPI.Create(varNovoItem)
ArrayAdd(arrayItensCriados, varItemCriado)

// Atualiza estoque do produto
AtualizarEstoque(arrayItens[i].id_produto, -arrayItens[i].quantidade)

Trace("✅ Item criado - ID: " + varItemCriado.id +
", Produto: " + arrayItens[i].id_produto +
", Quantidade: " + arrayItens[i].quantidade)
END

// 5. Monta resultado completo
varResult.pedido = varPedidoCriado
varResult.itens = arrayItensCriados
varResult.cliente = varCliente
varResult.resumo.total_itens = ArrayCount(arrayItensCriados)
varResult.resumo.valor_total = nValorTotal

Trace("🎉 Pedido completo criado com sucesso!")

RESULT varResult

EXCEPTION
Error("❌ Erro ao criar pedido: " + ExceptionInfo())

// Se houve erro e pedido foi criado, tenta cancelar
IF VariantExists(varPedidoCriado, "id") THEN
CancelarPedido(varPedidoCriado.id, "Erro durante criação")
END

RESULT Null
END
```

END

//——————————————————————————
// ObterPedidoCompleto: Busca pedido com todos os dados relacionados
//——————————————————————————
PROCEDURE ObterPedidoCompleto(sIDPedido is string) : Variant
varResult is Variant
varPedido is Variant
varCliente is Variant
arrayItens is array of Variant
arrayItensDetalhados is array of Variant
i is int

```
TRY
// 1. Busca dados do pedido
varPedido = m_cPedidoAPI.GetByID(sIDPedido)
IF VariantToJSON(varPedido) = "null" THEN
ExceptionThrow(1, "Pedido não encontrado: " + sIDPedido)
END

// 2. Busca dados do cliente
varCliente = m_cClienteAPI.GetByID(varPedido.id_cliente)

// 3. Busca itens do pedido
arrayItens = m_cItemPedidoAPI.GetAll("id_pedido=" + sIDPedido)

// 4. Enriquece itens com dados dos produtos
FOR i = 1 TO ArrayCount(arrayItens)
varItemDetalhado is Variant = arrayItens[i]

// Busca dados do produto
varProduto is Variant = m_cProdutoAPI.GetByID(arrayItens[i].id_produto)

// Adiciona informações do produto ao item
varItemDetalhado.produto_nome = varProduto.nome
varItemDetalhado.produto_descricao = varProduto.descricao
varItemDetalhado.produto_categoria = varProduto.categoria
varItemDetalhado.produto_estoque_atual = varProduto.estoque

ArrayAdd(arrayItensDetalhados, varItemDetalhado)
END

// 5. Monta resultado completo
varResult.pedido = varPedido
varResult.cliente = varCliente
varResult.itens = arrayItensDetalhados
varResult.resumo.total_itens = ArrayCount(arrayItensDetalhados)
varResult.resumo.valor_total = varPedido.valor_total

RESULT varResult

EXCEPTION
Error("❌ Erro ao obter pedido completo: " + ExceptionInfo())
RESULT Null
END
```

END

//——————————————————————————
// AtualizarStatusPedido: Atualiza status do pedido
//——————————————————————————
PROCEDURE AtualizarStatusPedido(sIDPedido is string, sNovoStatus is string, sObservacao is string = “”) : boolean
varDados is Variant

```
TRY
varDados.status = sNovoStatus

IF sObservacao <> "" THEN
// Busca observações atuais
varPedido is Variant = m_cPedidoAPI.GetByID(sIDPedido)
sObservacaoAtual is string = IIF(VariantExists(varPedido, "observacoes"), varPedido.observacoes, "")

// Adiciona nova observação com timestamp
sTimestamp is string = DateTimeToString(Now(), "DD/MM/YYYY HH:MM:SS")
varDados.observacoes = sObservacaoAtual + CR + "[" + sTimestamp + "] Status: " + sNovoStatus + " - " + sObservacao
END

varAtualizado is Variant = m_cPedidoAPI.Update(sIDPedido, varDados)

Trace("✅ Status do pedido " + sIDPedido + " atualizado para: " + sNovoStatus)
RESULT True

EXCEPTION
Error("❌ Erro ao atualizar status: " + ExceptionInfo())
RESULT False
END
```

END

//——————————————————————————
// CancelarPedido: Cancela pedido e reverte estoque
//——————————————————————————
PROCEDURE CancelarPedido(sIDPedido is string, sMotivo is string = “”) : boolean
arrayItens is array of Variant
i is int

```
TRY
// 1. Busca itens do pedido para reverter estoque
arrayItens = m_cItemPedidoAPI.GetAll("id_pedido=" + sIDPedido)

// 2. Reverte estoque de cada item
FOR i = 1 TO ArrayCount(arrayItens)
AtualizarEstoque(arrayItens[i].id_produto, arrayItens[i].quantidade)
Trace("↩️ Estoque revertido - Produto: " + arrayItens[i].id_produto + ", Qtd: " + arrayItens[i].quantidade)
END

// 3. Atualiza status do pedido
sObservacaoCancelamento is string = "Pedido cancelado"
IF sMotivo <> "" THEN
sObservacaoCancelamento += " - Motivo: " + sMotivo
END

RESULT AtualizarStatusPedido(sIDPedido, "CANCELADO", sObservacaoCancelamento)

EXCEPTION
Error("❌ Erro ao cancelar pedido: " + ExceptionInfo())
RESULT False
END
```

END

//==============================================================================
// MÉTODOS AUXILIARES
//==============================================================================

//——————————————————————————
// AtualizarEstoque: Atualiza estoque do produto
//——————————————————————————
PRIVATE PROCEDURE AtualizarEstoque(sIDProduto is string, nQuantidade is int) : boolean
TRY
// Busca produto atual
varProduto is Variant = m_cProdutoAPI.GetByID(sIDProduto)

```
// Calcula novo estoque
nNovoEstoque is int = varProduto.estoque + nQuantidade

IF nNovoEstoque < 0 THEN
ExceptionThrow(1, "Estoque não pode ficar negativo")
END

// Atualiza estoque
varDados is Variant
varDados.estoque = nNovoEstoque

m_cProdutoAPI.Update(sIDProduto, varDados)

Trace("📦 Estoque atualizado - Produto: " + sIDProduto +
", Anterior: " + varProduto.estoque +
", Novo: " + nNovoEstoque)

RESULT True

EXCEPTION
Error("❌ Erro ao atualizar estoque: " + ExceptionInfo())
RESULT False
END
```

END

//——————————————————————————
// ListarPedidosPorCliente: Lista pedidos de um cliente
//——————————————————————————
PROCEDURE ListarPedidosPorCliente(sIDCliente is string, sStatus is string = “”) : Variant
sFilter is string = “id_cliente=” + sIDCliente

```
IF sStatus <> "" THEN
sFilter += " AND status='" + sStatus + "'"
END

RESULT m_cPedidoAPI.GetAll(sFilter, 100, 0)
```

END

//——————————————————————————
// ListarPedidosPorPeriodo: Lista pedidos por período
//——————————————————————————
PROCEDURE ListarPedidosPorPeriodo(dDataInicio is Date, dDataFim is Date) : Variant
sFilter is string = StringBuild(“data_pedido>=’%1’ AND data_pedido<=’%2’”,
DateToString(dDataInicio, “YYYY-MM-DD”),
DateToString(dDataFim, “YYYY-MM-DD”))

```
RESULT m_cPedidoAPI.GetAll(sFilter, 1000, 0)
```

END

//==============================================================================
// EXEMPLO DE USO PRÁTICO
//==============================================================================

PROCEDURE TestarSistemaPedidos()
// Inicializa gerenciador
pedidoManager is CPedidoManager()

```
Info("🧪 Iniciando teste do Sistema de Pedidos...")

TRY
//----------------------------------------------------------------------
// 1. CRIAR DADOS DE TESTE
//----------------------------------------------------------------------
Info("📝 Criando dados de teste...")

// Criar cliente de teste
novoCliente is Variant
novoCliente.nome = "João Silva"
novoCliente.email = "joao.teste@email.com"
novoCliente.telefone = "11999887766"
novoCliente.endereco = "Rua das Flores, 123"
novoCliente.cidade = "São Paulo"

clienteCriado is Variant = pedidoManager.m_cClienteAPI.Create(novoCliente)
sIDCliente is string = clienteCriado.id

// Criar produtos de teste
arrayProdutosCriados is array of string

FOR i = 1 TO 3
novoProduto is Variant
novoProduto.nome = "Produto Teste " + i
novoProduto.descricao = "Descrição do produto " + i
novoProduto.preco = 25.50 * i
novoProduto.estoque = 100
novoProduto.categoria = "Eletrônicos"

produtoCriado is Variant = pedidoManager.m_cProdutoAPI.Create(novoProduto)
ArrayAdd(arrayProdutosCriados, produtoCriado.id)
END

Info("✅ Dados de teste criados - Cliente: " + sIDCliente + ", Produtos: " + ArrayCount(arrayProdutosCriados))

//----------------------------------------------------------------------
// 2. CRIAR PEDIDO COMPLETO
//----------------------------------------------------------------------
Info("🛒 Criando pedido completo...")

pedidoCompleto is Variant
pedidoCompleto.id_cliente = sIDCliente
pedidoCompleto.observacoes = "Pedido de teste criado via API"

// Itens do pedido
arrayItens is array of Variant

FOR i = 1 TO ArrayCount(arrayProdutosCriados)
novoItem is Variant
novoItem.id_produto = arrayProdutosCriados[i]
novoItem.quantidade = i * 2 // 2, 4, 6
novoItem.observacoes = "Item " + i + " do pedido de teste"

ArrayAdd(arrayItens, novoItem)
END

pedidoCompleto.itens = arrayItens

// Cria pedido
resultadoPedido is Variant = pedidoManager.CriarPedidoCompleto(pedidoCompleto)
sIDPedido is string = resultadoPedido.pedido.id

sResumo is string = StringBuild("✅ Pedido criado com sucesso!" + CR +
"• ID: %1" + CR +
"• Cliente: %2" + CR +
"• Total de itens: %3" + CR +
"• Valor total: R$ %4",
sIDPedido,
resultadoPedido.cliente.nome,
resultadoPedido.resumo.total_itens,
resultadoPedido.resumo.valor_total)
Info(sResumo)

//----------------------------------------------------------------------
// 3. CONSULTAR PEDIDO COMPLETO
//----------------------------------------------------------------------
Info("🔍 Consultando pedido completo...")

pedidoDetalhado is Variant = pedidoManager.ObterPedidoCompleto(sIDPedido)

sDetalhes is string = "📋 Detalhes do Pedido:" + CR + CR
sDetalhes += "Cliente: " + pedidoDetalhado.cliente.nome + " (" + pedidoDetalhado.cliente.email + ")" + CR
sDetalhes += "Data: " + pedidoDetalhado.pedido.data_pedido + " " + pedidoDetalhado.pedido.hora_pedido + CR
sDetalhes += "Status: " + pedidoDetalhado.pedido.status + CR
sDetalhes += "Valor Total: R$ " + pedidoDetalhado.pedido.valor_total + CR + CR
sDetalhes += "📦 Itens:" + CR

FOR i = 1 TO ArrayCount(pedidoDetalhado.itens)
sDetalhes += StringBuild("• %1 (Qtd: %2, Preço: R$ %3, Total: R$ %4)" + CR,
pedidoDetalhado.itens[i].produto_nome,
pedidoDetalhado.itens[i].quantidade,
pedidoDetalhado.itens[i].preco_unitario,
pedidoDetalhado.itens[i].preco_total)
END

Info(sDetalhes)

//----------------------------------------------------------------------
// 4. ATUALIZAR STATUS DO PEDIDO
//----------------------------------------------------------------------
Info("📝 Atualizando status do pedido...")

IF pedidoManager.AtualizarStatusPedido(sIDPedido, "CONFIRMADO", "Pedido confirmado após verificação") THEN
Info("✅ Status atualizado para CONFIRMADO")
END

IF pedidoManager.AtualizarStatusPedido(sIDPedido, "ENVIADO", "Pedido enviado via transportadora XYZ") THEN
Info("✅ Status atualizado para ENVIADO")
END

//----------------------------------------------------------------------
// 5. LISTAR PEDIDOS DO CLIENTE
//----------------------------------------------------------------------
Info("📋 Listando pedidos do cliente...")

pedidosCliente is Variant = pedidoManager.ListarPedidosPorCliente(sIDCliente)
Info("📊 Cliente possui " + ArrayCount(pedidosCliente) + " pedido(s)")

//----------------------------------------------------------------------
// 6. CANCELAR PEDIDO (OPCIONAL)
//----------------------------------------------------------------------
nResposta is int = YesNo("❓ Deseja cancelar o pedido de teste?")

IF nResposta = Yes THEN
IF pedidoManager.CancelarPedido(sIDPedido, "Teste concluído") THEN
Info("✅ Pedido cancelado e estoque revertido")
END
END

//----------------------------------------------------------------------
// 7. LIMPEZA DOS DADOS DE TESTE
//----------------------------------------------------------------------
IF YesNo("🗑️ Excluir dados de teste criados?") = Yes THEN
Info("🗑️ Removendo dados de teste...")

// Remove itens do pedido
itensPedido is Variant = pedidoManager.m_cItemPedidoAPI.GetAll("id_pedido=" + sIDPedido)
FOR i = 1 TO ArrayCount(itensPedido)
pedidoManager.m_cItemPedidoAPI.Delete(itensPedido[i].id)
END

// Remove pedido
pedidoManager.m_cPedidoAPI.Delete(sIDPedido)

// Remove produtos
FOR i = 1 TO ArrayCount(arrayProdutosCriados)
pedidoManager.m_cProdutoAPI.Delete(arrayProdutosCriados[i])
END

// Remove cliente
pedidoManager.m_cClienteAPI.Delete(sIDCliente)

Info("✅ Dados de teste removidos")
END

EXCEPTION
Error("❌ Erro durante teste: " + ExceptionInfo())
END

Info("🎉 Teste do Sistema de Pedidos concluído!")
```

END

//==============================================================================
// RELATÓRIOS E CONSULTAS AVANÇADAS
//==============================================================================

//——————————————————————————
// GerarRelatorioPedidos: Gera relatório consolidado de pedidos
//——————————————————————————
PROCEDURE GerarRelatorioPedidos(dDataInicio is Date, dDataFim is Date) : string
pedidoManager is CPedidoManager()
sRelatorio is string

```
TRY
// Busca pedidos do período
pedidosPeriodo is Variant = pedidoManager.ListarPedidosPorPeriodo(dDataInicio, dDataFim)

nTotalPedidos is int = ArrayCount(pedidosPeriodo)
nValorTotal is currency = 0
nPendentes is int = 0
nConfirmados is int = 0
nEnviados is int = 0
nEntregues is int = 0
nCancelados is int = 0

// Analisa dados
FOR i = 1 TO nTotalPedidos
nValorTotal += pedidosPeriodo[i].valor_total

SWITCH pedidosPeriodo[i].status
CASE "PENDENTE": nPendentes++
CASE "CONFIRMADO": nConfirmados++
CASE "ENVIADO": nEnviados++
CASE "ENTREGUE": nEntregues++
CASE "CANCELADO": nCancelados++
END
END

// Gera relatório
sRelatorio = "📊 RELATÓRIO DE PEDIDOS" + CR
sRelatorio += "Período: " + DateToString(dDataInicio, "DD/MM/YYYY") + " a " + DateToString(dDataFim, "DD/MM/YYYY") + CR
sRelatorio += StringRepeat("=", 50) + CR + CR

sRelatorio += "📈 RESUMO GERAL:" + CR
sRelatorio += "• Total de pedidos: " + nTotalPedidos + CR
sRelatorio += "• Valor total: R$ " + NumToString(nValorTotal, "999,999.99") + CR
sRelatorio += "• Ticket médio: R$ " + IIF(nTotalPedidos > 0, NumToString(nValorTotal / nTotalPedidos, "999.99"), "0.00") + CR + CR

sRelatorio += "📊 STATUS DOS PEDIDOS:" + CR
sRelatorio += "• Pendentes: " + nPendentes + " (" + NumToString(nPendentes * 100 / nTotalPedidos, "99.9") + "%)" + CR
sRelatorio += "• Confirmados: " + nConfirmados + " (" + NumToString(nConfirmados *
```



//==============================================================================
// EXEMPLO COMPLETO: SISTEMA DE PEDIDOS E ITENS
// Descrição: Demonstra uso prático com relacionamentos mestre-detalhe
// Tabelas: PEDIDO, ITEM_PEDIDO, CLIENTE, PRODUTO
// Autor: Sistema Automático
// Data: 14/08/2025
//==============================================================================

//==============================================================================
// ESTRUTURA DAS TABELAS (Para referência)
//==============================================================================
/*
CLIENTE:

- id (Numeric, Primary Key, Auto Increment)
- nome (Text 100, Mandatory)
- email (Text 150, Mandatory, Unique)
- telefone (Text 20)
- endereco (Text 200)
- cidade (Text 50)
- ativo (Boolean, Default True)

PRODUTO:

- id (Numeric, Primary Key, Auto Increment)
- nome (Text 100, Mandatory)
- descricao (Text 500)
- preco (Currency, Mandatory)
- estoque (Numeric, Default 0)
- categoria (Text 50)
- ativo (Boolean, Default True)

PEDIDO:

- id (Numeric, Primary Key, Auto Increment)
- id_cliente (Numeric, Foreign Key -> CLIENTE.id, Mandatory)
- data_pedido (Date, Mandatory, Default Today)
- hora_pedido (Time, Default Now)
- status (Text 20, Default “PENDENTE”) // PENDENTE, CONFIRMADO, ENVIADO, ENTREGUE, CANCELADO
- valor_total (Currency, Default 0)
- observacoes (Text 500)
- data_entrega (Date)

ITEM_PEDIDO:

- id (Numeric, Primary Key, Auto Increment)
- id_pedido (Numeric, Foreign Key -> PEDIDO.id, Mandatory)
- id_produto (Numeric, Foreign Key -> PRODUTO.id, Mandatory)
- quantidade (Numeric, Mandatory, Default 1)
- preco_unitario (Currency, Mandatory)
- preco_total (Currency, Calculated: quantidade * preco_unitario)
- observacoes (Text 200)
*/

//==============================================================================
// SERVIDOR: INICIALIZANDO APIS PARA TODAS AS TABELAS
//==============================================================================

PROCEDURE IniciarServidorPedidos()
// Gerenciador para todas as tabelas do sistema
manager is CRestCRUDManager()

```
Info("🚀 Iniciando Sistema de Pedidos - APIs REST")

// Exibe documentação das tabelas encontradas
sDoc is string = manager.GetAPIDocumentation()
Trace("📋 Tabelas do sistema:" + CR + sDoc)

// Inicia servidores para todas as tabelas
IF manager.StartAllServers() THEN
Info("✅ Servidor de Pedidos iniciado com sucesso!" + CR + CR + ...
"🌐 APIs disponíveis:" + CR + ...
"• CLIENTE: http://localhost:8080/api/v1/cliente" + CR + ...
"• PRODUTO: http://localhost:8081/api/v1/produto" + CR + ...
"• PEDIDO: http://localhost:8082/api/v1/pedido" + CR + ...
"• ITEM_PEDIDO: http://localhost:8083/api/v1/item_pedido" + CR + CR + ...
"📖 Consulte API_Documentation.md para detalhes")

// Mantém servidores ativos
Info("⚡ Servidores rodando... Pressione OK para parar.")
ELSE
Error("❌ Falha ao iniciar servidores!")
END
```

END

//==============================================================================
// CLASSE: CPedidoManager
// Descrição: Gerenciador de alto nível para operações de pedidos
//==============================================================================

CPedidoManager is Class
PRIVATE
m_cClienteAPI is CRestCRUDClient
m_cProdutoAPI is CRestCRUDClient
m_cPedidoAPI is CRestCRUDClient
m_cItemPedidoAPI is CRestCRUDClient
m_sBaseURL is string
END

//——————————————————————————
// Constructor: Inicializa todos os clientes API
//——————————————————————————
PROCEDURE Constructor(sBaseURL is string = “http://localhost”)
m_sBaseURL = sBaseURL

```
// Inicializa clientes para cada tabela
m_cClienteAPI = new CRestCRUDClient(sBaseURL + ":8080/api/v1", "cliente")
m_cProdutoAPI = new CRestCRUDClient(sBaseURL + ":8081/api/v1", "produto")
m_cPedidoAPI = new CRestCRUDClient(sBaseURL + ":8082/api/v1", "pedido")
m_cItemPedidoAPI = new CRestCRUDClient(sBaseURL + ":8083/api/v1", "item_pedido")

// Timeout de 60 segundos para operações complexas
m_cClienteAPI.SetTimeout(60000)
m_cProdutoAPI.SetTimeout(60000)
m_cPedidoAPI.SetTimeout(60000)
m_cItemPedidoAPI.SetTimeout(60000)
```

END

//==============================================================================
// MÉTODOS DE NEGÓCIO - PEDIDOS COMPLETOS
//==============================================================================

//——————————————————————————
// CriarPedidoCompleto: Cria pedido com itens em uma transação
//——————————————————————————
PROCEDURE CriarPedidoCompleto(varPedidoCompleto is Variant) : Variant
varPedidoCriado is Variant
varResult is Variant
arrayItens is array of Variant
nValorTotal is currency = 0
i is int

```
TRY
Trace("🛒 Iniciando criação de pedido completo...")

// 1. Valida se cliente existe
varCliente is Variant = m_cClienteAPI.GetByID(varPedidoCompleto.id_cliente)
IF VariantToJSON(varCliente) = "null" THEN
ExceptionThrow(1, "Cliente não encontrado: " + varPedidoCompleto.id_cliente)
END

Trace("✅ Cliente validado: " + varCliente.nome)

// 2. Valida produtos e calcula valor total
IF VariantExists(varPedidoCompleto, "itens") THEN
arrayItens = varPedidoCompleto.itens

FOR i = 1 TO ArrayCount(arrayItens)
// Valida produto
varProduto is Variant = m_cProdutoAPI.GetByID(arrayItens[i].id_produto)
IF VariantToJSON(varProduto) = "null" THEN
ExceptionThrow(2, "Produto não encontrado: " + arrayItens[i].id_produto)
END

// Valida estoque
IF varProduto.estoque < arrayItens[i].quantidade THEN
ExceptionThrow(3, "Estoque insuficiente para produto: " + varProduto.nome +
" (Disponível: " + varProduto.estoque +
", Solicitado: " + arrayItens[i].quantidade + ")")
END

// Usa preço atual do produto se não informado
IF NOT VariantExists(arrayItens[i], "preco_unitario") THEN
arrayItens[i].preco_unitario = varProduto.preco
END

// Calcula preço total do item
arrayItens[i].preco_total = arrayItens[i].quantidade * arrayItens[i].preco_unitario
nValorTotal += arrayItens[i].preco_total

Trace("✅ Item validado: " + varProduto.nome +
" (Qtd: " + arrayItens[i].quantidade +
", Valor: " + arrayItens[i].preco_total + ")")
END
END

// 3. Cria o pedido (cabeçalho)
varNovoPedido is Variant
varNovoPedido.id_cliente = varPedidoCompleto.id_cliente
varNovoPedido.data_pedido = IIF(VariantExists(varPedidoCompleto, "data_pedido"),
varPedidoCompleto.data_pedido,
DateToString(Today(), "YYYY-MM-DD"))
varNovoPedido.hora_pedido = IIF(VariantExists(varPedidoCompleto, "hora_pedido"),
varPedidoCompleto.hora_pedido,
TimeToString(Now(), "HH:MM:SS"))
varNovoPedido.status = IIF(VariantExists(varPedidoCompleto, "status"),
varPedidoCompleto.status,
"PENDENTE")
varNovoPedido.valor_total = nValorTotal
varNovoPedido.observacoes = IIF(VariantExists(varPedidoCompleto, "observacoes"),
varPedidoCompleto.observacoes,
"")

varPedidoCriado = m_cPedidoAPI.Create(varNovoPedido)

Trace("✅ Pedido criado - ID: " + varPedidoCriado.id + ", Valor: " + varPedidoCriado.valor_total)

// 4. Cria os itens do pedido
arrayItensCriados is array of Variant

FOR i = 1 TO ArrayCount(arrayItens)
varNovoItem is Variant
varNovoItem.id_pedido = varPedidoCriado.id
varNovoItem.id_produto = arrayItens[i].id_produto
varNovoItem.quantidade = arrayItens[i].quantidade
varNovoItem.preco_unitario = arrayItens[i].preco_unitario
varNovoItem.preco_total = arrayItens[i].preco_total
varNovoItem.observacoes = IIF(VariantExists(arrayItens[i], "observacoes"),
arrayItens[i].observacoes,
"")

varItemCriado is Variant = m_cItemPedidoAPI.Create(varNovoItem)
ArrayAdd(arrayItensCriados, varItemCriado)

// Atualiza estoque do produto
AtualizarEstoque(arrayItens[i].id_produto, -arrayItens[i].quantidade)

Trace("✅ Item criado - ID: " + varItemCriado.id +
", Produto: " + arrayItens[i].id_produto +
", Quantidade: " + arrayItens[i].quantidade)
END

// 5. Monta resultado completo
varResult.pedido = varPedidoCriado
varResult.itens = arrayItensCriados
varResult.cliente = varCliente
varResult.resumo.total_itens = ArrayCount(arrayItensCriados)
varResult.resumo.valor_total = nValorTotal

Trace("🎉 Pedido completo criado com sucesso!")

RESULT varResult

EXCEPTION
Error("❌ Erro ao criar pedido: " + ExceptionInfo())

// Se houve erro e pedido foi criado, tenta cancelar
IF VariantExists(varPedidoCriado, "id") THEN
CancelarPedido(varPedidoCriado.id, "Erro durante criação")
END

RESULT Null
END
```

END

//——————————————————————————
// ObterPedidoCompleto: Busca pedido com todos os dados relacionados
//——————————————————————————
PROCEDURE ObterPedidoCompleto(sIDPedido is string) : Variant
varResult is Variant
varPedido is Variant
varCliente is Variant
arrayItens is array of Variant
arrayItensDetalhados is array of Variant
i is int

```
TRY
// 1. Busca dados do pedido
varPedido = m_cPedidoAPI.GetByID(sIDPedido)
IF VariantToJSON(varPedido) = "null" THEN
ExceptionThrow(1, "Pedido não encontrado: " + sIDPedido)
END

// 2. Busca dados do cliente
varCliente = m_cClienteAPI.GetByID(varPedido.id_cliente)

// 3. Busca itens do pedido
arrayItens = m_cItemPedidoAPI.GetAll("id_pedido=" + sIDPedido)

// 4. Enriquece itens com dados dos produtos
FOR i = 1 TO ArrayCount(arrayItens)
varItemDetalhado is Variant = arrayItens[i]

// Busca dados do produto
varProduto is Variant = m_cProdutoAPI.GetByID(arrayItens[i].id_produto)

// Adiciona informações do produto ao item
varItemDetalhado.produto_nome = varProduto.nome
varItemDetalhado.produto_descricao = varProduto.descricao
varItemDetalhado.produto_categoria = varProduto.categoria
varItemDetalhado.produto_estoque_atual = varProduto.estoque

ArrayAdd(arrayItensDetalhados, varItemDetalhado)
END

// 5. Monta resultado completo
varResult.pedido = varPedido
varResult.cliente = varCliente
varResult.itens = arrayItensDetalhados
varResult.resumo.total_itens = ArrayCount(arrayItensDetalhados)
varResult.resumo.valor_total = varPedido.valor_total

RESULT varResult

EXCEPTION
Error("❌ Erro ao obter pedido completo: " + ExceptionInfo())
RESULT Null
END
```

END

//——————————————————————————
// AtualizarStatusPedido: Atualiza status do pedido
//——————————————————————————
PROCEDURE AtualizarStatusPedido(sIDPedido is string, sNovoStatus is string, sObservacao is string = “”) : boolean
varDados is Variant

```
TRY
varDados.status = sNovoStatus

IF sObservacao <> "" THEN
// Busca observações atuais
varPedido is Variant = m_cPedidoAPI.GetByID(sIDPedido)
sObservacaoAtual is string = IIF(VariantExists(varPedido, "observacoes"), varPedido.observacoes, "")

// Adiciona nova observação com timestamp
sTimestamp is string = DateTimeToString(Now(), "DD/MM/YYYY HH:MM:SS")
varDados.observacoes = sObservacaoAtual + CR + "[" + sTimestamp + "] Status: " + sNovoStatus + " - " + sObservacao
END

varAtualizado is Variant = m_cPedidoAPI.Update(sIDPedido, varDados)

Trace("✅ Status do pedido " + sIDPedido + " atualizado para: " + sNovoStatus)
RESULT True

EXCEPTION
Error("❌ Erro ao atualizar status: " + ExceptionInfo())
RESULT False
END
```

END

//——————————————————————————
// CancelarPedido: Cancela pedido e reverte estoque
//——————————————————————————
PROCEDURE CancelarPedido(sIDPedido is string, sMotivo is string = “”) : boolean
arrayItens is array of Variant
i is int

```
TRY
// 1. Busca itens do pedido para reverter estoque
arrayItens = m_cItemPedidoAPI.GetAll("id_pedido=" + sIDPedido)

// 2. Reverte estoque de cada item
FOR i = 1 TO ArrayCount(arrayItens)
AtualizarEstoque(arrayItens[i].id_produto, arrayItens[i].quantidade)
Trace("↩️ Estoque revertido - Produto: " + arrayItens[i].id_produto + ", Qtd: " + arrayItens[i].quantidade)
END

// 3. Atualiza status do pedido
sObservacaoCancelamento is string = "Pedido cancelado"
IF sMotivo <> "" THEN
sObservacaoCancelamento += " - Motivo: " + sMotivo
END

RESULT AtualizarStatusPedido(sIDPedido, "CANCELADO", sObservacaoCancelamento)

EXCEPTION
Error("❌ Erro ao cancelar pedido: " + ExceptionInfo())
RESULT False
END
```

END

//==============================================================================
// MÉTODOS AUXILIARES
//==============================================================================

//——————————————————————————
// AtualizarEstoque: Atualiza estoque do produto
//——————————————————————————
PRIVATE PROCEDURE AtualizarEstoque(sIDProduto is string, nQuantidade is int) : boolean
TRY
// Busca produto atual
varProduto is Variant = m_cProdutoAPI.GetByID(sIDProduto)

```
// Calcula novo estoque
nNovoEstoque is int = varProduto.estoque + nQuantidade

IF nNovoEstoque < 0 THEN
ExceptionThrow(1, "Estoque não pode ficar negativo")
END

// Atualiza estoque
varDados is Variant
varDados.estoque = nNovoEstoque

m_cProdutoAPI.Update(sIDProduto, varDados)

Trace("📦 Estoque atualizado - Produto: " + sIDProduto +
", Anterior: " + varProduto.estoque +
", Novo: " + nNovoEstoque)

RESULT True

EXCEPTION
Error("❌ Erro ao atualizar estoque: " + ExceptionInfo())
RESULT False
END
```

END

//——————————————————————————
// ListarPedidosPorCliente: Lista pedidos de um cliente
//——————————————————————————
PROCEDURE ListarPedidosPorCliente(sIDCliente is string, sStatus is string = “”) : Variant
sFilter is string = “id_cliente=” + sIDCliente

```
IF sStatus <> "" THEN
sFilter += " AND status='" + sStatus + "'"
END

RESULT m_cPedidoAPI.GetAll(sFilter, 100, 0)
```

END

//——————————————————————————
// ListarPedidosPorPeriodo: Lista pedidos por período
//——————————————————————————
PROCEDURE ListarPedidosPorPeriodo(dDataInicio is Date, dDataFim is Date) : Variant
sFilter is string = StringBuild(“data_pedido>=’%1’ AND data_pedido<=’%2’”,
DateToString(dDataInicio, “YYYY-MM-DD”),
DateToString(dDataFim, “YYYY-MM-DD”))

```
RESULT m_cPedidoAPI.GetAll(sFilter, 1000, 0)
```

END

//==============================================================================
// EXEMPLO DE USO PRÁTICO
//==============================================================================

PROCEDURE TestarSistemaPedidos()
// Inicializa gerenciador
pedidoManager is CPedidoManager()

```
Info("🧪 Iniciando teste do Sistema de Pedidos...")

TRY
//----------------------------------------------------------------------
// 1. CRIAR DADOS DE TESTE
//----------------------------------------------------------------------
Info("📝 Criando dados de teste...")

// Criar cliente de teste
novoCliente is Variant
novoCliente.nome = "João Silva"
novoCliente.email = "joao.teste@email.com"
novoCliente.telefone = "11999887766"
novoCliente.endereco = "Rua das Flores, 123"
novoCliente.cidade = "São Paulo"

clienteCriado is Variant = pedidoManager.m_cClienteAPI.Create(novoCliente)
sIDCliente is string = clienteCriado.id

// Criar produtos de teste
arrayProdutosCriados is array of string

FOR i = 1 TO 3
novoProduto is Variant
novoProduto.nome = "Produto Teste " + i
novoProduto.descricao = "Descrição do produto " + i
novoProduto.preco = 25.50 * i
novoProduto.estoque = 100
novoProduto.categoria = "Eletrônicos"

produtoCriado is Variant = pedidoManager.m_cProdutoAPI.Create(novoProduto)
ArrayAdd(arrayProdutosCriados, produtoCriado.id)
END

Info("✅ Dados de teste criados - Cliente: " + sIDCliente + ", Produtos: " + ArrayCount(arrayProdutosCriados))

//----------------------------------------------------------------------
// 2. CRIAR PEDIDO COMPLETO
//----------------------------------------------------------------------
Info("🛒 Criando pedido completo...")

pedidoCompleto is Variant
pedidoCompleto.id_cliente = sIDCliente
pedidoCompleto.observacoes = "Pedido de teste criado via API"

// Itens do pedido
arrayItens is array of Variant

FOR i = 1 TO ArrayCount(arrayProdutosCriados)
novoItem is Variant
novoItem.id_produto = arrayProdutosCriados[i]
novoItem.quantidade = i * 2 // 2, 4, 6
novoItem.observacoes = "Item " + i + " do pedido de teste"

ArrayAdd(arrayItens, novoItem)
END

pedidoCompleto.itens = arrayItens

// Cria pedido
resultadoPedido is Variant = pedidoManager.CriarPedidoCompleto(pedidoCompleto)
sIDPedido is string = resultadoPedido.pedido.id

sResumo is string = StringBuild("✅ Pedido criado com sucesso!" + CR +
"• ID: %1" + CR +
"• Cliente: %2" + CR +
"• Total de itens: %3" + CR +
"• Valor total: R$ %4",
sIDPedido,
resultadoPedido.cliente.nome,
resultadoPedido.resumo.total_itens,
resultadoPedido.resumo.valor_total)
Info(sResumo)

//----------------------------------------------------------------------
// 3. CONSULTAR PEDIDO COMPLETO
//----------------------------------------------------------------------
Info("🔍 Consultando pedido completo...")

pedidoDetalhado is Variant = pedidoManager.ObterPedidoCompleto(sIDPedido)

sDetalhes is string = "📋 Detalhes do Pedido:" + CR + CR
sDetalhes += "Cliente: " + pedidoDetalhado.cliente.nome + " (" + pedidoDetalhado.cliente.email + ")" + CR
sDetalhes += "Data: " + pedidoDetalhado.pedido.data_pedido + " " + pedidoDetalhado.pedido.hora_pedido + CR
sDetalhes += "Status: " + pedidoDetalhado.pedido.status + CR
sDetalhes += "Valor Total: R$ " + pedidoDetalhado.pedido.valor_total + CR + CR
sDetalhes += "📦 Itens:" + CR

FOR i = 1 TO ArrayCount(pedidoDetalhado.itens)
sDetalhes += StringBuild("• %1 (Qtd: %2, Preço: R$ %3, Total: R$ %4)" + CR,
pedidoDetalhado.itens[i].produto_nome,
pedidoDetalhado.itens[i].quantidade,
pedidoDetalhado.itens[i].preco_unitario,
pedidoDetalhado.itens[i].preco_total)
END

Info(sDetalhes)

//----------------------------------------------------------------------
// 4. ATUALIZAR STATUS DO PEDIDO
//----------------------------------------------------------------------
Info("📝 Atualizando status do pedido...")

IF pedidoManager.AtualizarStatusPedido(sIDPedido, "CONFIRMADO", "Pedido confirmado após verificação") THEN
Info("✅ Status atualizado para CONFIRMADO")
END

IF pedidoManager.AtualizarStatusPedido(sIDPedido, "ENVIADO", "Pedido enviado via transportadora XYZ") THEN
Info("✅ Status atualizado para ENVIADO")
END

//----------------------------------------------------------------------
// 5. LISTAR PEDIDOS DO CLIENTE
//----------------------------------------------------------------------
Info("📋 Listando pedidos do cliente...")

pedidosCliente is Variant = pedidoManager.ListarPedidosPorCliente(sIDCliente)
Info("📊 Cliente possui " + ArrayCount(pedidosCliente) + " pedido(s)")

//----------------------------------------------------------------------
// 6. CANCELAR PEDIDO (OPCIONAL)
//----------------------------------------------------------------------
nResposta is int = YesNo("❓ Deseja cancelar o pedido de teste?")

IF nResposta = Yes THEN
IF pedidoManager.CancelarPedido(sIDPedido, "Teste concluído") THEN
Info("✅ Pedido cancelado e estoque revertido")
END
END

//----------------------------------------------------------------------
// 7. LIMPEZA DOS DADOS DE TESTE
//----------------------------------------------------------------------
IF YesNo("🗑️ Excluir dados de teste criados?") = Yes THEN
Info("🗑️ Removendo dados de teste...")

// Remove itens do pedido
itensPedido is Variant = pedidoManager.m_cItemPedidoAPI.GetAll("id_pedido=" + sIDPedido)
FOR i = 1 TO ArrayCount(itensPedido)
pedidoManager.m_cItemPedidoAPI.Delete(itensPedido[i].id)
END

// Remove pedido
pedidoManager.m_cPedidoAPI.Delete(sIDPedido)

// Remove produtos
FOR i = 1 TO ArrayCount(arrayProdutosCriados)
pedidoManager.m_cProdutoAPI.Delete(arrayProdutosCriados[i])
END

// Remove cliente
pedidoManager.m_cClienteAPI.Delete(sIDCliente)

Info("✅ Dados de teste removidos")
END

EXCEPTION
Error("❌ Erro durante teste: " + ExceptionInfo())
END

Info("🎉 Teste do Sistema de Pedidos concluído!")
```

END

//==============================================================================
// RELATÓRIOS E CONSULTAS AVANÇADAS
//==============================================================================

//——————————————————————————
// GerarRelatorioPedidos: Gera relatório consolidado de pedidos
//——————————————————————————
PROCEDURE GerarRelatorioPedidos(dDataInicio is Date, dDataFim is Date) : string
pedidoManager is CPedidoManager()
sRelatorio is string

```
TRY
// Busca pedidos do período
pedidosPeriodo is Variant = pedidoManager.ListarPedidosPorPeriodo(dDataInicio, dDataFim)

nTotalPedidos is int = ArrayCount(pedidosPeriodo)
nValorTotal is currency = 0
nPendentes is int = 0
nConfirmados is int = 0
nEnviados is int = 0
nEntregues is int = 0
nCancelados is int = 0

// Analisa dados
FOR i = 1 TO nTotalPedidos
nValorTotal += pedidosPeriodo[i].valor_total

SWITCH pedidosPeriodo[i].status
CASE "PENDENTE": nPendentes++
CASE "CONFIRMADO": nConfirmados++
CASE "ENVIADO": nEnviados++
CASE "ENTREGUE": nEntregues++
CASE "CANCELADO": nCancelados++
END
END

// Gera relatório
sRelatorio = "📊 RELATÓRIO DE PEDIDOS" + CR
sRelatorio += "Período: " + DateToString(dDataInicio, "DD/MM/YYYY") + " a " + DateToString(dDataFim, "DD/MM/YYYY") + CR
sRelatorio += StringRepeat("=", 50) + CR + CR

sRelatorio += "📈 RESUMO GERAL:" + CR
sRelatorio += "• Total de pedidos: " + nTotalPedidos + CR
sRelatorio += "• Valor total: R$ " + NumToString(nValorTotal, "999,999.99") + CR
sRelatorio += "• Ticket médio: R$ " + IIF(nTotalPedidos > 0, NumToString(nValorTotal / nTotalPedidos, "999.99"), "0.00") + CR + CR

sRelatorio += "📊 STATUS DOS PEDIDOS:" + CR
sRelatorio += "• Pendentes: " + nPendentes + " (" + NumToString(nPendentes * 100 / nTotalPedidos, "99.9") + "%)" + CR
sRelatorio += "• Confirmados: " + nConfirmados + " (" + NumToString(nConfirmados * 100 / nTotalPedidos, "99.9") + "%)" + CR
sRelatorio += "• Enviados: " + nEnviados + " (" + NumToString(nEnviados * 100 / nTotalPedidos, "99.9") + "%)" + CR
sRelatorio += "• Entregues: " + nEntregues + " (" + NumToString(nEntregues * 100 / nTotalPedidos, "99.9") + "%)" + CR
sRelatorio += "• Cancelados: " + nCancelados + " (" + NumToString(nCancelados * 100 / nTotalPedidos, "99.9") + "%)" + CR + CR

RESULT sRelatorio

EXCEPTION
Error("❌ Erro ao gerar relatório: " + ExceptionInfo())
RESULT "Erro ao gerar relatório"
END
```

END

//==============================================================================
// CLASSE: CPedidoWebService
// Descrição: WebService específico para operações de pedidos
//==============================================================================

CPedidoWebService is Class
PRIVATE
m_cPedidoManager is CPedidoManager
END

//——————————————————————————
// Constructor
//——————————————————————————
PROCEDURE Constructor()
m_cPedidoManager = new CPedidoManager()
END

//——————————————————————————
// ProcessarPedidoWeb: Endpoint personalizado para pedidos via web
//——————————————————————————
PROCEDURE ProcessarPedidoWeb()
sJSONInput is string
varPedidoCompleto is Variant
varResultado is Variant
sResponse is string

```
// Configurar CORS
WebWriteHTTPHeader("Access-Control-Allow-Origin", "*")
WebWriteHTTPHeader("Access-Control-Allow-Methods", "POST, OPTIONS")
WebWriteHTTPHeader("Access-Control-Allow-Headers", "Content-Type, Authorization")

TRY
// Trata OPTIONS (preflight)
IF WebRequestMethod() = "OPTIONS" THEN
WebWriteHTTPCode(204)
RETURN
END

// Obtém dados do pedido
sJSONInput = WebReadRequestBody()

IF sJSONInput = "" THEN
WebWriteHTTPCode(400)
WebWriteString("{\"error\": \"Dados do pedido não informados\"}", "application/json")
RETURN
END

// Converte JSON para Variant
JSONToVariant(varPedidoCompleto, sJSONInput)

// Valida dados obrigatórios
IF NOT VariantExists(varPedidoCompleto, "id_cliente") THEN
WebWriteHTTPCode(400)
WebWriteString("{\"error\": \"ID do cliente é obrigatório\"}", "application/json")
RETURN
END

IF NOT VariantExists(varPedidoCompleto, "itens") OR ArrayCount(varPedidoCompleto.itens) = 0 THEN
WebWriteHTTPCode(400)
WebWriteString("{\"error\": \"Pelo menos um item é obrigatório\"}", "application/json")
RETURN
END

// Processa pedido
varResultado = m_cPedidoManager.CriarPedidoCompleto(varPedidoCompleto)

IF VariantToJSON(varResultado) = "null" THEN
WebWriteHTTPCode(500)
WebWriteString("{\"error\": \"Erro interno ao processar pedido\"}", "application/json")
RETURN
END

// Retorna sucesso
sResponse = VariantToJSON(varResultado)
WebWriteHTTPCode(201)
WebWriteString(sResponse, "application/json")

EXCEPTION
WebWriteHTTPCode(500)
WebWriteString("{\"error\": \"" + Replace(ExceptionInfo(), """", "\""") + "\"}", "application/json")
END
```

END

//——————————————————————————
// ConsultarPedidoWeb: Endpoint para consulta de pedido
//——————————————————————————
PROCEDURE ConsultarPedidoWeb()
sIDPedido is string
varResultado is Variant
sResponse is string

```
// Configurar CORS
WebWriteHTTPHeader("Access-Control-Allow-Origin", "*")
WebWriteHTTPHeader("Access-Control-Allow-Methods", "GET, OPTIONS")
WebWriteHTTPHeader("Access-Control-Allow-Headers", "Content-Type")

TRY
IF WebRequestMethod() = "OPTIONS" THEN
WebWriteHTTPCode(204)
RETURN
END

sIDPedido = WebParameter("id", parameterURL)

IF sIDPedido = "" THEN
WebWriteHTTPCode(400)
WebWriteString("{\"error\": \"ID do pedido não informado\"}", "application/json")
RETURN
END

varResultado = m_cPedidoManager.ObterPedidoCompleto(sIDPedido)

IF VariantToJSON(varResultado) = "null" THEN
WebWriteHTTPCode(404)
WebWriteString("{\"error\": \"Pedido não encontrado\"}", "application/json")
RETURN
END

sResponse = VariantToJSON(varResultado)
WebWriteHTTPCode(200)
WebWriteString(sResponse, "application/json")

EXCEPTION
WebWriteHTTPCode(500)
WebWriteString("{\"error\": \"" + Replace(ExceptionInfo(), """", "\""") + "\"}", "application/json")
END
```

END

//==============================================================================
// INTEGRAÇÃO COM E-COMMERCE
//==============================================================================

//——————————————————————————
// ProcessarPedidoEcommerce: Processa pedido vindo de e-commerce
//——————————————————————————
PROCEDURE ProcessarPedidoEcommerce(sJSONPedido is string) : string
varPedidoEcommerce is Variant
varPedidoInterno is Variant
varResultado is Variant
pedidoManager is CPedidoManager()
i is int

```
TRY
// Parse do JSON do e-commerce
JSONToVariant(varPedidoEcommerce, sJSONPedido)

// Mapeia dados do e-commerce para formato interno
varPedidoInterno.id_cliente = ObterOuCriarCliente(varPedidoEcommerce.cliente)
varPedidoInterno.observacoes = "Pedido E-commerce #" + varPedidoEcommerce.numero_pedido

// Mapeia itens
arrayItensInternos is array of Variant

FOR i = 1 TO ArrayCount(varPedidoEcommerce.itens)
varItemInterno is Variant
varItemInterno.id_produto = ObterProdutoPorSKU(varPedidoEcommerce.itens[i].sku)
varItemInterno.quantidade = varPedidoEcommerce.itens[i].quantidade
varItemInterno.preco_unitario = varPedidoEcommerce.itens[i].preco
varItemInterno.observacoes = "SKU: " + varPedidoEcommerce.itens[i].sku

ArrayAdd(arrayItensInternos, varItemInterno)
END

varPedidoInterno.itens = arrayItensInternos

// Cria pedido
varResultado = pedidoManager.CriarPedidoCompleto(varPedidoInterno)

// Retorna resposta para e-commerce
varResposta is Variant
varResposta.sucesso = True
varResposta.id_pedido_interno = varResultado.pedido.id
varResposta.numero_pedido_ecommerce = varPedidoEcommerce.numero_pedido
varResposta.valor_total = varResultado.resumo.valor_total
varResposta.mensagem = "Pedido processado com sucesso"

RESULT VariantToJSON(varResposta)

EXCEPTION
// Retorna erro para e-commerce
varErro is Variant
varErro.sucesso = False
varErro.erro = ExceptionInfo()
varErro.numero_pedido_ecommerce = IIF(VariantExists(varPedidoEcommerce, "numero_pedido"),
varPedidoEcommerce.numero_pedido,
"N/A")

RESULT VariantToJSON(varErro)
END
```

END

//——————————————————————————
// ObterOuCriarCliente: Obtém cliente existente ou cria novo
//——————————————————————————
PRIVATE PROCEDURE ObterOuCriarCliente(varClienteEcommerce is Variant) : string
clienteAPI is CRestCRUDClient(“http://localhost:8080/api/v1”, “cliente”)

```
TRY
// Busca cliente por email
clientesEncontrados is Variant = clienteAPI.GetAll("email='" + varClienteEcommerce.email + "'")

IF ArrayCount(clientesEncontrados) > 0 THEN
// Cliente existe, retorna ID
RESULT clientesEncontrados[1].id
ELSE
// Cria novo cliente
novoCliente is Variant
novoCliente.nome = varClienteEcommerce.nome
novoCliente.email = varClienteEcommerce.email
novoCliente.telefone = IIF(VariantExists(varClienteEcommerce, "telefone"), varClienteEcommerce.telefone, "")
novoCliente.endereco = IIF(VariantExists(varClienteEcommerce, "endereco"), varClienteEcommerce.endereco, "")
novoCliente.cidade = IIF(VariantExists(varClienteEcommerce, "cidade"), varClienteEcommerce.cidade, "")

clienteCriado is Variant = clienteAPI.Create(novoCliente)
RESULT clienteCriado.id
END

EXCEPTION
ExceptionThrow(1, "Erro ao obter/criar cliente: " + ExceptionInfo())
END
```

END

//——————————————————————————
// ObterProdutoPorSKU: Obtém ID do produto pelo SKU
//——————————————————————————
PRIVATE PROCEDURE ObterProdutoPorSKU(sSKU is string) : string
produtoAPI is CRestCRUDClient(“http://localhost:8081/api/v1”, “produto”)

```
TRY
// Busca produto pelo SKU (assumindo que existe campo SKU)
produtosEncontrados is Variant = produtoAPI.GetAll("sku='" + sSKU + "'")

IF ArrayCount(produtosEncontrados) > 0 THEN
RESULT produtosEncontrados[1].id
ELSE
ExceptionThrow(1, "Produto não encontrado para SKU: " + sSKU)
END

EXCEPTION
ExceptionThrow(2, "Erro ao buscar produto por SKU: " + ExceptionInfo())
END
```

END

//==============================================================================
// EXEMPLO DE INTEGRAÇÃO COM JAVASCRIPT/WEB
//==============================================================================

/*
EXEMPLO DE CÓDIGO JAVASCRIPT PARA CONSUMIR A API:

// Função para criar pedido via JavaScript
async function criarPedido() {
const pedido = {
id_cliente: “1”,
observacoes: “Pedido criado via JavaScript”,
itens: [
{
id_produto: “1”,
quantidade: 2,
observacoes: “Item 1”
},
{
id_produto: “2”,
quantidade: 1,
preco_unitario: 99.90,
observacoes: “Item 2 com preço especial”
}
]
};

```
try {
const response = await fetch('http://localhost:8082/api/v1/pedido-completo', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(pedido)
});

if (response.ok) {
const resultado = await response.json();
console.log('✅ Pedido criado:', resultado);

// Exibe resultado
document.getElementById('resultado').innerHTML = `
<h3>Pedido Criado com Sucesso!</h3>
<p><strong>ID:</strong> ${resultado.pedido.id}</p>
<p><strong>Cliente:</strong> ${resultado.cliente.nome}</p>
<p><strong>Total de Itens:</strong> ${resultado.resumo.total_itens}</p>
<p><strong>Valor Total:</strong> R$ ${resultado.resumo.valor_total}</p>
`;
} else {
const erro = await response.json();
console.error('❌ Erro:', erro);
alert('Erro ao criar pedido: ' + erro.error);
}
} catch (error) {
console.error('❌ Erro de rede:', error);
alert('Erro de conexão: ' + error.message);
}
```

}

// Função para consultar pedido
async function consultarPedido(idPedido) {
try {
const response = await fetch(`http://localhost:8082/api/v1/pedido-completo/${idPedido}`);

```
if (response.ok) {
const pedido = await response.json();
console.log('📋 Pedido encontrado:', pedido);

// Monta HTML com detalhes
let html = `
<h3>Detalhes do Pedido #${pedido.pedido.id}</h3>
<p><strong>Cliente:</strong> ${pedido.cliente.nome} (${pedido.cliente.email})</p>
<p><strong>Data:</strong> ${pedido.pedido.data_pedido} ${pedido.pedido.hora_pedido}</p>
<p><strong>Status:</strong> ${pedido.pedido.status}</p>
<p><strong>Valor Total:</strong> R$ ${pedido.pedido.valor_total}</p>
<h4>Itens:</h4>
<ul>
`;

pedido.itens.forEach(item => {
html += `
<li>
${item.produto_nome} -
Qtd: ${item.quantidade} -
Preço: R$ ${item.preco_unitario} -
Total: R$ ${item.preco_total}
</li>
`;
});

html += '</ul>';
document.getElementById('detalhes').innerHTML = html;

} else {
alert('Pedido não encontrado');
}
} catch (error) {
console.error('❌ Erro:', error);
alert('Erro ao consultar pedido: ' + error.message);
}
```

}
*/

//==============================================================================
// CONFIGURAÇÃO DO SERVIDOR WEB PERSONALIZADO
//==============================================================================

PROCEDURE ConfigurarServidorPedidos()
// Cria instância do WebService
pedidoWS is CPedidoWebService()

```
// Configura rotas personalizadas
WebAddRoute("/api/v1/pedido-completo", httpPost, pedidoWS.ProcessarPedidoWeb)
WebAddRoute("/api/v1/pedido-completo", httpOptions, pedidoWS.ProcessarPedidoWeb)
WebAddRoute("/api/v1/pedido-completo/{id}", httpGet, pedidoWS.ConsultarPedidoWeb)
WebAddRoute("/api/v1/pedido-completo/{id}", httpOptions, pedidoWS.ConsultarPedidoWeb)

// Inicia servidor na porta 8090 para endpoints personalizados
IF WebServerStart(8090) THEN
Info("🌐 Servidor de Pedidos Personalizados iniciado na porta 8090" + CR + CR +
"Endpoints disponíveis:" + CR +
"• POST http://localhost:8090/api/v1/pedido-completo" + CR +
"• GET http://localhost:8090/api/v1/pedido-completo/{id}")
ELSE
Error("❌ Falha ao iniciar servidor personalizado")
END
```

END

//==============================================================================
// EXEMPLO DE JSON PARA TESTE
//==============================================================================

/*
EXEMPLO DE JSON PARA CRIAR PEDIDO:

{
“id_cliente”: “1”,
“observacoes”: “Pedido via API REST”,
“itens”: [
{
“id_produto”: “1”,
“quantidade”: 2,
“observacoes”: “Produto principal”
},
{
“id_produto”: “2”,
“quantidade”: 1,
“preco_unitario”: 99.90,
“observacoes”: “Produto com desconto”
},
{
“id_produto”: “3”,
“quantidade”: 3
}
]
}

RESPOSTA ESPERADA:

{
“pedido”: {
“id”: “15”,
“id_cliente”: “1”,
“data_pedido”: “2025-08-14”,
“hora_pedido”: “14:30:25”,
“status”: “PENDENTE”,
“valor_total”: 285.30,
“observacoes”: “Pedido via API REST”
},
“cliente”: {
“id”: “1”,
“nome”: “João Silva”,
“email”: “joao@email.com”
},
“itens”: [
{
“id”: “45”,
“id_pedido”: “15”,
“id_produto”: “1”,
“quantidade”: 2,
“preco_unitario”: 50.00,
“preco_total”: 100.00,
“produto_nome”: “Produto A”
},
{
“id”: “46”,
“id_pedido”: “15”,
“id_produto”: “2”,
“quantidade”: 1,
“preco_unitario”: 99.90,
“preco_total”: 99.90,
“produto_nome”: “Produto B”
}
],
“resumo”: {
“total_itens”: 2,
“valor_total”: 285.30
}
}
*/




📋 Exemplo Completo de Sistema de Pedidos e Itens
🏗️ Estrutura do Sistema:
1. 4 Tabelas principais:
• CLIENTE - Dados dos clientes
• PRODUTO - Catálogo de produtos
• PEDIDO - Cabeçalho dos pedidos
• ITEM_PEDIDO - Itens de cada pedido
2. 3 Classes especializadas:
• CPedidoManager - Operações de negócio de alto nível
• CPedidoWebService - Endpoints web personalizados
• APIs CRUD automáticas para cada tabela
🚀 Funcionalidades Implementadas:
✅ Operações Completas de Pedido:
• Criação transacional - Pedido + itens em uma operação
• Validação automática - Cliente, produtos, estoque
• Cálculo automático - Preços e totais
• Controle de estoque - Baixa automática e reversão
• Consulta enriquecida - Pedido com dados relacionados
✅ Gestão de Status:
• Fluxo completo: PENDENTE → CONFIRMADO → ENVIADO → ENTREGUE
• Cancelamento inteligente - Reverte estoque automaticamente
• Histórico de alterações - Log com timestamps
✅ Integrações:
• E-commerce - Processa pedidos externos
• JavaScript/Web - APIs prontas para frontend
• Relatórios - Análises automáticas

🎯 Casos de Uso Práticos:

1. Criação de Pedido Simples:

pedidoManager is CPedidoManager()

// Monta pedido
pedidoCompleto is Variant
pedidoCompleto.id_cliente = "1"
pedidoCompleto.observacoes = "Pedido urgente"

// Adiciona itens
arrayItens is array of Variant
novoItem is Variant
novoItem.id_produto = "5"
novoItem.quantidade = 3
ArrayAdd(arrayItens, novoItem)

pedidoCompleto.itens = arrayItens

// Cria pedido completo
resultado is Variant = pedidoManager.CriarPedidoCompleto(pedidoCompleto)



2. Integração com E-commerce:

sJSONEcommerce = `{
"numero_pedido": "EC-2025-001",
"cliente": {
"nome": "Maria Silva",
"email": "maria@email.com"
},
"itens": [
{"sku": "PROD-001", "quantidade": 2, "preco": 50.00}
]
}`

sResposta = ProcessarPedidoEcommerce(sJSONEcommerce)



3. Consulta Avançada:

// Busca pedido com todos os detalhes
pedidoCompleto is Variant = pedidoManager.ObterPedidoCompleto("15")

// Dados disponíveis:
// pedidoCompleto.pedido - Dados do pedido
// pedidoCompleto.cliente - Dados do cliente
// pedidoCompleto.itens[i].produto_nome - Nome dos produtos
// pedidoCompleto.resumo.valor_total - Totais calculados



🌐 APIs REST Disponíveis:
APIs Básicas (CRUD automático):
http://localhost:8080/api/v1/cliente
http://localhost:8081/api/v1/produto
http://localhost:8082/api/v1/pedido
http://localhost:8083/api/v1/item_pedido
APIs Especializadas:
• POST http://localhost:8090/api/v1/pedido-completo - Criar pedido completo
• GET http://localhost:8090/api/v1/pedido-completo/{id} - Consultar com detalhes
📊 Recursos Avançados:
• Relatórios automáticos com estatísticas
• Controle transacional - Rollback em caso de erro
• Validações de negócio - Estoque, cliente, produtos
• Logs detalhados - Rastreabilidade completa
• CORS configurado - Pronto para web
• Tratamento de erros robusto

🔧 Fácil de Usar:

Para iniciar tudo:

// Inicia todas as APIs básicas
IniciarServidorPedidos()

// Inicia APIs personalizadas
ConfigurarServidorPedidos()

// Testa o sistema completo
TestarSistemaPedidos()


Este exemplo demonstra como o sistema CRUD REST API em WLanguage pode ser usado para casos reais complexos, mantendo a simplicidade de uso e máximo reaproveitamento de código! 🎯​​​​​​​​​​​​​​​​


//==============================================================================
// RECURSOS AVANÇADOS E EXTENSÕES DO SISTEMA CRUD REST API
// Descrição: Cache, Autenticação, Validações, Hooks, Performance
// Autor: Sistema Automático
// Data: 14/08/2025
//==============================================================================

//==============================================================================
// CLASSE: CRestCacheManager
// Descrição: Gerenciamento de cache para otimização de performance
//==============================================================================

CRestCacheManager is Class
PRIVATE
m_stCache is array of stCacheEntry
m_nMaxEntries is int = 1000
m_nDefaultTTL is int = 300 // 5 minutos em segundos
END

stCacheEntry is Structure
sKey is string
varData is Variant
dtExpiry is DateTime
nHits is int
dtLastAccess is DateTime
END

//——————————————————————————
// Get: Obtém item do cache
//——————————————————————————
PROCEDURE Get(sKey is string) : Variant
i is int
dtNow is DateTime = Now()

```
FOR i = 1 TO ArrayCount(m_stCache)
IF m_stCache[i].sKey = sKey THEN
// Verifica se não expirou
IF m_stCache[i].dtExpiry > dtNow THEN
m_stCache[i].nHits++
m_stCache[i].dtLastAccess = dtNow
Trace("🎯 Cache HIT: " + sKey)
RESULT m_stCache[i].varData
ELSE
// Remove item expirado
ArrayDelete(m_stCache, i)
Trace("⏰ Cache EXPIRED: " + sKey)
BREAK
END
END
END

Trace("❌ Cache MISS: " + sKey)
RESULT Null
```

END

//——————————————————————————
// Set: Armazena item no cache
//——————————————————————————
PROCEDURE Set(sKey is string, varData is Variant, nTTLSeconds is int = 0)
stEntry is stCacheEntry
dtNow is DateTime = Now()

```
// Remove entrada existente
RemoveByKey(sKey)

// Limpa cache se estiver cheio
IF ArrayCount(m_stCache) >= m_nMaxEntries THEN
CleanupOldEntries()
END

// Adiciona nova entrada
stEntry.sKey = sKey
stEntry.varData = varData
stEntry.dtExpiry = DateTimeAdd(dtNow, IIF(nTTLSeconds > 0, nTTLSeconds, m_nDefaultTTL), "s")
stEntry.nHits = 0
stEntry.dtLastAccess = dtNow

ArrayAdd(m_stCache, stEntry)
Trace("✅ Cache SET: " + sKey + " (TTL: " + IIF(nTTLSeconds > 0, nTTLSeconds, m_nDefaultTTL) + "s)")
```

END

//——————————————————————————
// InvalidatePattern: Invalida itens por padrão
//——————————————————————————
PROCEDURE InvalidatePattern(sPattern is string)
i is int = 1
nRemoved is int = 0

```
WHILE i <= ArrayCount(m_stCache)
IF Position(m_stCache[i].sKey, sPattern) > 0 THEN
ArrayDelete(m_stCache, i)
nRemoved++
ELSE
i++
END
END

Trace("🗑️ Cache invalidated: " + nRemoved + " entries matching '" + sPattern + "'")
```

END

//——————————————————————————
// RemoveByKey: Remove item específico do cache
//——————————————————————————
PRIVATE PROCEDURE RemoveByKey(sKey is string)
i is int

```
FOR i = 1 TO ArrayCount(m_stCache)
IF m_stCache[i].sKey = sKey THEN
ArrayDelete(m_stCache, i)
BREAK
END
END
```

END

//——————————————————————————
// CleanupOldEntries: Remove entradas antigas para liberar espaço
//——————————————————————————
PRIVATE PROCEDURE CleanupOldEntries()
dtNow is DateTime = Now()
i is int = 1
nRemoved is int = 0

```
// Remove entradas expiradas primeiro
WHILE i <= ArrayCount(m_stCache)
IF m_stCache[i].dtExpiry <= dtNow THEN
ArrayDelete(m_stCache, i)
nRemoved++
ELSE
i++
END
END

// Se ainda estiver cheio, remove as menos acessadas
IF ArrayCount(m_stCache) >= m_nMaxEntries THEN
ArraySort(m_stCache, asSortByMember, "dtLastAccess")

WHILE ArrayCount(m_stCache) >= m_nMaxEntries * 0.8
ArrayDelete(m_stCache, 1)
nRemoved++
END
END

Trace("🧹 Cache cleanup: " + nRemoved + " entries removed")
```

END

//==============================================================================
// CLASSE: CRestAuthManager
// Descrição: Gerenciamento de autenticação e autorização
//==============================================================================

CRestAuthManager is Class
PRIVATE
m_stTokens is array of stAuthToken
m_stUsers is array of stAuthUser
m_sJWTSecret is string = “MinhaChaveSecreta123!”
m_nTokenExpiry is int = 3600 // 1 hora
END

stAuthToken is Structure
sToken is string
sUserID is string
dtExpiry is DateTime
sPermissions is string
END

stAuthUser is Structure
sUserID is string
sUsername is string
sPasswordHash is string
sPermissions is string
bActive is boolean
END

//——————————————————————————
// AuthenticateUser: Autentica usuário e gera token
//——————————————————————————
PROCEDURE AuthenticateUser(sUsername is string, sPassword is string) : string
i is int
sPasswordHash is string
sToken is string
stToken is stAuthToken

```
TRY
// Hash da senha fornecida
sPasswordHash = Encrypt(sPassword, encryptMD5)

// Busca usuário
FOR i = 1 TO ArrayCount(m_stUsers)
IF m_stUsers[i].sUsername = sUsername AND m_stUsers[i].sPasswordHash = sPasswordHash AND m_stUsers[i].bActive THEN
// Gera token JWT
sToken = GenerateJWTToken(m_stUsers[i].sUserID, m_stUsers[i].sPermissions)

// Armazena token
stToken.sToken = sToken
stToken.sUserID = m_stUsers[i].sUserID
stToken.dtExpiry = DateTimeAdd(Now(), m_nTokenExpiry, "s")
stToken.sPermissions = m_stUsers[i].sPermissions

ArrayAdd(m_stTokens, stToken)

Trace("✅ User authenticated: " + sUsername + " (Token: " + Left(sToken, 20) + "...)")
RESULT sToken
END
END

Trace("❌ Authentication failed: " + sUsername)
RESULT ""

EXCEPTION
Error("Erro na autenticação: " + ExceptionInfo())
RESULT ""
END
```

END

//——————————————————————————
// ValidateToken: Valida token de acesso
//——————————————————————————
PROCEDURE ValidateToken(sToken is string) : stAuthToken
i is int
dtNow is DateTime = Now()
stEmptyToken is stAuthToken

```
FOR i = 1 TO ArrayCount(m_stTokens)
IF m_stTokens[i].sToken = sToken THEN
IF m_stTokens[i].dtExpiry > dtNow THEN
Trace("✅ Token valid: " + Left(sToken, 20) + "...")
RESULT m_stTokens[i]
ELSE
// Remove token expirado
ArrayDelete(m_stTokens, i)
Trace("⏰ Token expired: " + Left(sToken, 20) + "...")
BREAK
END
END
END

Trace("❌ Token invalid: " + Left(sToken, 20) + "...")
RESULT stEmptyToken
```

END

//——————————————————————————
// CheckPermission: Verifica se usuário tem permissão
//——————————————————————————
PROCEDURE CheckPermission(sToken is string, sRequiredPermission is string) : boolean
stToken is stAuthToken = ValidateToken(sToken)

```
IF stToken.sUserID = "" THEN
RESULT False
END

// Verifica se tem permissão específica ou é admin
IF Position(stToken.sPermissions, sRequiredPermission) > 0 OR Position(stToken.sPermissions, "admin") > 0 THEN
RESULT True
END

Trace("🚫 Permission denied: " + sRequiredPermission + " for user " + stToken.sUserID)
RESULT False
```

END

//——————————————————————————
// CreateUser: Cria novo usuário
//——————————————————————————
PROCEDURE CreateUser(sUsername is string, sPassword is string, sPermissions is string = “read”) : boolean
stUser is stAuthUser

```
TRY
stUser.sUserID = GenerateGUID()
stUser.sUsername = sUsername
stUser.sPasswordHash = Encrypt(sPassword, encryptMD5)
stUser.sPermissions = sPermissions
stUser.bActive = True

ArrayAdd(m_stUsers, stUser)

Trace("✅ User created: " + sUsername + " (Permissions: " + sPermissions + ")")
RESULT True

EXCEPTION
Error("Erro ao criar usuário: " + ExceptionInfo())
RESULT False
END
```

END

//——————————————————————————
// GenerateJWTToken: Gera token JWT simples
//——————————————————————————
PRIVATE PROCEDURE GenerateJWTToken(sUserID is string, sPermissions is string) : string
sHeader is string = “{"typ":"JWT","alg":"HS256"}”
sPayload is string = StringBuild(”{"sub":"%1","permissions":"%2","exp":%3,"iat":%4}”,
sUserID,
sPermissions,
DateTimeToInteger(DateTimeAdd(Now(), m_nTokenExpiry, “s”)),
DateTimeToInteger(Now()))

```
sHeaderB64 is string = Encode(sHeader, encodeBASE64URL)
sPayloadB64 is string = Encode(sPayload, encodeBASE64URL)

sSignature is string = Encrypt(sHeaderB64 + "." + sPayloadB64 + "." + m_sJWTSecret, encryptSHA256)
sSignatureB64 is string = Encode(sSignature, encodeBASE64URL)

RESULT sHeaderB64 + "." + sPayloadB64 + "." + sSignatureB64
```

END

//==============================================================================
// CLASSE: CRestValidationManager
// Descrição: Validações customizadas e regras de negócio
//==============================================================================

CRestValidationManager is Class
PRIVATE
m_stRules is array of stValidationRule
END

stValidationRule is Structure
sTable is string
sField is string
sRuleType is string // required, email, numeric, date, custom
sCustomFunction is string
sErrorMessage is string
bActive is boolean
END

//——————————————————————————
// AddRule: Adiciona regra de validação
//——————————————————————————
PROCEDURE AddRule(sTable is string, sField is string, sRuleType is string, sErrorMessage is string, sCustomFunction is string = “”)
stRule is stValidationRule

```
stRule.sTable = Upper(sTable)
stRule.sField = Upper(sField)
stRule.sRuleType = sRuleType
stRule.sCustomFunction = sCustomFunction
stRule.sErrorMessage = sErrorMessage
stRule.bActive = True

ArrayAdd(m_stRules, stRule)

Trace("📋 Validation rule added: " + sTable + "." + sField + " (" + sRuleType + ")")
```

END

//——————————————————————————
// ValidateRecord: Valida registro contra regras
//——————————————————————————
PROCEDURE ValidateRecord(sTable is string, varRecord is Variant, arrayErrors is array of string) : boolean
i is int
bValid is boolean = True
sValue is string

```
ArrayDeleteAll(arrayErrors)

FOR i = 1 TO ArrayCount(m_stRules)
IF m_stRules[i].bActive AND Upper(m_stRules[i].sTable) = Upper(sTable) THEN

IF VariantExists(varRecord, m_stRules[i].sField) THEN
sValue = varRecord[m_stRules[i].sField]
ELSE
sValue = ""
END

IF NOT ValidateField(sValue, m_stRules[i]) THEN
ArrayAdd(arrayErrors, m_stRules[i].sErrorMessage)
bValid = False
END
END
END

RESULT bValid
```

END

//——————————————————————————
// ValidateField: Valida campo individual
//——————————————————————————
PRIVATE PROCEDURE ValidateField(sValue is string, stRule is stValidationRule) : boolean
SWITCH stRule.sRuleType
CASE “required”
RESULT sValue <> “”

```
CASE "email"
RESULT EmailValid(sValue)

CASE "numeric"
RESULT IsNumeric(sValue)

CASE "date"
RESULT DateValid(sValue)

CASE "custom"
IF stRule.sCustomFunction <> "" THEN
// Executa função customizada usando indirection
RESULT {stRule.sCustomFunction}(sValue)
END
RESULT True

DEFAULT
RESULT True
END
```

END

//==============================================================================
// CLASSE: CRestHookManager
// Descrição: Sistema de hooks para interceptar operações CRUD
//==============================================================================

CRestHookManager is Class
PRIVATE
m_stHooks is array of stHook
END

stHook is Structure
sTable is string
sOperation is string // CREATE, READ, UPDATE, DELETE
sWhen is string // BEFORE, AFTER
sFunctionName is string
bActive is boolean
END

//——————————————————————————
// RegisterHook: Registra hook para interceptar operações
//——————————————————————————
PROCEDURE RegisterHook(sTable is string, sOperation is string, sWhen is string, sFunctionName is string)
stHook is stHook

```
stHook.sTable = Upper(sTable)
stHook.sOperation = Upper(sOperation)
stHook.sWhen = Upper(sWhen)
stHook.sFunctionName = sFunctionName
stHook.bActive = True

ArrayAdd(m_stHooks, stHook)

Trace("🎣 Hook registered: " + sTable + "." + sOperation + "." + sWhen + " -> " + sFunctionName)
```

END

//——————————————————————————
// ExecuteHooks: Executa hooks para operação
//——————————————————————————
PROCEDURE ExecuteHooks(sTable is string, sOperation is string, sWhen is string, varData is Variant) : boolean
i is int
bResult is boolean = True

```
FOR i = 1 TO ArrayCount(m_stHooks)
IF m_stHooks[i].bActive AND
Upper(m_stHooks[i].sTable) = Upper(sTable) AND
Upper(m_stHooks[i].sOperation) = Upper(sOperation) AND
Upper(m_stHooks[i].sWhen) = Upper(sWhen) THEN

TRY
// Executa hook usando indirection
bResult = {m_stHooks[i].sFunctionName}(sTable, sOperation, sWhen, varData)

IF NOT bResult THEN
Trace("🚫 Hook blocked operation: " + m_stHooks[i].sFunctionName)
BREAK
END

EXCEPTION
Error("Erro no hook " + m_stHooks[i].sFunctionName + ": " + ExceptionInfo())
bResult = False
BREAK
END
END
END

RESULT bResult
```

END

//==============================================================================
// CLASSE ESTENDIDA: CRestCRUDServerAdvanced
// Descrição: Servidor CRUD com recursos avançados
//==============================================================================

CRestCRUDServerAdvanced is Class inherits from CRestCRUDServer
PRIVATE
m_cCache is CRestCacheManager
m_cAuth is CRestAuthManager
m_cValidation is CRestValidationManager
m_cHooks is CRestHookManager
m_bCacheEnabled is boolean = True
m_bAuthRequired is boolean = False
m_bValidationEnabled is boolean = True
END

//——————————————————————————
// Constructor: Inicializa servidor avançado
//——————————————————————————
PROCEDURE Constructor(sTableOrQuery is string, sConnection is string = “”)
// Chama constructor da classe pai
Ancestor:Constructor(sTableOrQuery, sConnection)

```
// Inicializa componentes avançados
m_cCache = new CRestCacheManager()
m_cAuth = new CRestAuthManager()
m_cValidation = new CRestValidationManager()
m_cHooks = new CRestHookManager()

// Configura validações padrão
SetupDefaultValidations()

// Cria usuário admin padrão
m_cAuth.CreateUser("admin", "admin123", "admin,read,write,delete")
m_cAuth.CreateUser("guest", "guest", "read")

Trace("🚀 Advanced CRUD Server initialized for: " + sTableOrQuery)
```

END

//——————————————————————————
// EnableCache: Habilita/desabilita cache
//——————————————————————————
PROCEDURE EnableCache(bEnable is boolean)
m_bCacheEnabled = bEnable
Trace(“🎯 Cache “ + IIF(bEnable, “enabled”, “disabled”))
END

//——————————————————————————
// RequireAuth: Habilita/desabilita autenticação
//——————————————————————————
PROCEDURE RequireAuth(bRequire is boolean)
m_bAuthRequired = bRequire
Trace(“🔐 Authentication “ + IIF(bRequire, “required”, “optional”))
END

//——————————————————————————
// GetAllRecordsAdvanced: GET com recursos avançados
//——————————————————————————
PROCEDURE GetAllRecordsAdvanced()
sToken is string
sCacheKey is string
varCachedData is Variant
sJSON is string

```
// Verifica autenticação se necessária
IF m_bAuthRequired THEN
sToken = WebParameter("Authorization", parameterHTTPHeader)
sToken = Replace(sToken, "Bearer ", "")

IF NOT m_cAuth.CheckPermission(sToken, "read") THEN
HandleAPIError("Acesso negado", 401)
RETURN
END
END

// Verifica cache se habilitado
IF m_bCacheEnabled THEN
sCacheKey = "GET_ALL_" + m_sTableName + "_" + WebParameter("filter", parameterGet) + "_" + WebParameter("limit", parameterGet) + "_" + WebParameter("offset", parameterGet)
varCachedData = m_cCache.Get(sCacheKey)

IF VariantToJSON(varCachedData) <> "null" THEN
SetCORSHeaders()
WebWriteHTTPCode(200)
WebWriteHTTPHeader("X-Cache", "HIT")
WebWriteString(varCachedData, "application/json")
RETURN
END
END

// Executa hooks BEFORE
varHookData is Variant
IF NOT m_cHooks.ExecuteHooks(m_sTableName, "READ", "BEFORE", varHookData) THEN
HandleAPIError("Operação bloqueada por hook", 403)
RETURN
END

// Chama método original
GetAllRecords()

// Cache da resposta se habilitado
IF m_bCacheEnabled THEN
// Obter resposta gerada (simplificado)
sJSON = GenerateRecordsJSON(WebParameter("filter", parameterGet), Val(WebParameter("limit", parameterGet, "100")), Val(WebParameter("offset", parameterGet, "0")))
m_cCache.Set(sCacheKey, sJSON, 300) // 5 minutos
WebWriteHTTPHeader("X-Cache", "MISS")
END

// Executa hooks AFTER
m_cHooks.ExecuteHooks(m_sTableName, "READ", "AFTER", varHookData)
```

END

//——————————————————————————
// CreateRecordAdvanced: POST com validações e hooks
//——————————————————————————
PROCEDURE CreateRecordAdvanced()
sToken is string
sJSONInput is string
varRecord is Variant
arrayErrors is array of string

```
// Verifica autenticação
IF m_bAuthRequired THEN
sToken = WebParameter("Authorization", parameterHTTPHeader)
sToken = Replace(sToken, "Bearer ", "")

IF NOT m_cAuth.CheckPermission(sToken, "write") THEN
HandleAPIError("Acesso negado", 401)
RETURN
END
END

TRY
sJSONInput = WebReadRequestBody()
JSONToVariant(varRecord, sJSONInput)

// Executa hooks BEFORE
IF NOT m_cHooks.ExecuteHooks(m_sTableName, "CREATE", "BEFORE", varRecord) THEN
HandleAPIError("Operação bloqueada por hook", 403)
RETURN
END

// Validações customizadas
IF m_bValidationEnabled THEN
IF NOT m_cValidation.ValidateRecord(m_sTableName, varRecord, arrayErrors) THEN
sErrorMessage is string = "Erro de validação:" + CR
FOR i = 1 TO ArrayCount(arrayErrors)
sErrorMessage += "• " + arrayErrors[i] + CR
END

HandleAPIError(sErrorMessage, 400)
RETURN
END
END

// Cria registro (método original)
sResponse is string = InsertRecord(varRecord)

// Invalida cache
IF m_bCacheEnabled THEN
m_cCache.InvalidatePattern(m_sTableName)
END

// Executa hooks AFTER
JSONToVariant(varRecord, sResponse)
m_cHooks.ExecuteHooks(m_sTableName, "CREATE", "AFTER", varRecord)

SetCORSHeaders()
WebWriteHTTPCode(201)
WebWriteString(sResponse, "application/json")

EXCEPTION
HandleAPIError("Erro ao criar registro: " + ExceptionInfo(), 500)
END
```

END

//——————————————————————————
// SetupDefaultValidations: Configura validações padrão
//——————————————————————————
PRIVATE PROCEDURE SetupDefaultValidations()
i is int

```
// Adiciona validações automáticas baseadas na análise
FOR i = 1 TO ArrayCount(m_stFieldsInfo)
// Campos obrigatórios
IF m_stFieldsInfo[i].bMandatory THEN
m_cValidation.AddRule(m_sTableName, m_stFieldsInfo[i].sName, "required",
"Campo '" + m_stFieldsInfo[i].sName + "' é obrigatório")
END

// Validações por tipo
SWITCH m_stFieldsInfo[i].nType
CASE hItemNumeric, hItemReal, hItemCurrency
m_cValidation.AddRule(m_sTableName, m_stFieldsInfo[i].sName, "numeric",
"Campo '" + m_stFieldsInfo[i].sName + "' deve ser numérico")

CASE hItemDate
m_cValidation.AddRule(m_sTableName, m_stFieldsInfo[i].sName, "date",
"Campo '" + m_stFieldsInfo[i].sName + "' deve ser uma data válida")
END

// Campos de email (baseado no nome)
IF Position(Lower(m_stFieldsInfo[i].sName), "email") > 0 THEN
m_cValidation.AddRule(m_sTableName, m_stFieldsInfo[i].sName, "email",
"Campo '" + m_stFieldsInfo[i].sName + "' deve ser um email válido")
END
END
```

END

//==============================================================================
// FUNÇÕES DE HOOK EXEMPLOS
//==============================================================================

//——————————————————————————
// HookClienteBeforeCreate: Hook executado antes de criar cliente
//——————————————————————————
PROCEDURE HookClienteBeforeCreate(sTable is string, sOperation is string, sWhen is string, varData is Variant) : boolean
// Exemplo: Formatar dados antes de inserir

```
// Converte nome para maiúsculo
IF VariantExists(varData, "nome") THEN
varData.nome = Upper(varData.nome)
END

// Formata telefone
IF VariantExists(varData, "telefone") THEN
sTelefone is string = varData.telefone
sTelefone = Replace(sTelefone, "(", "")
sTelefone = Replace(sTelefone, ")", "")
sTelefone = Replace(sTelefone, "-", "")
sTelefone = Replace(sTelefone, " ", "")
varData.telefone = sTelefone
END

// Adiciona data de cadastro se não informada
IF NOT VariantExists(varData, "data_cadastro") THEN
varData.data_cadastro = DateToString(Today(), "YYYY-MM-DD")
END

Trace("🎣 Hook ClienteBeforeCreate executed - Data formatted")
RESULT True
```

END

//——————————————————————————
// HookProdutoAfterUpdate: Hook executado após atualizar produto
//——————————————————————————
PROCEDURE HookProdutoAfterUpdate(sTable is string, sOperation is string, sWhen is string, varData is Variant) : boolean
// Exemplo: Notificação quando estoque fica baixo

```
IF VariantExists(varData, "estoque") AND VariantExists(varData, "estoque_minimo") THEN
IF varData.estoque <= varData.estoque_minimo THEN
// Simula envio de notificação
sNotificacao is string = StringBuild("⚠️ ALERTA: Produto '%1' com estoque baixo (%2 unidades)",
varData.nome, varData.estoque)

Trace(sNotificacao)

// Aqui poderia enviar email, SMS, etc.
// EnviarNotificacao("estoque_baixo", sNotificacao)
END
END

RESULT True
```

END

//==============================================================================
// EXEMPLO DE USO DO SERVIDOR AVANÇADO
//==============================================================================

PROCEDURE TestarServidorAvancado()
Info(“🧪 Testando Servidor CRUD Avançado…”)

```
TRY
//----------------------------------------------------------------------
// 1. CRIAR SERVIDOR AVANÇADO PARA CLIENTE
//----------------------------------------------------------------------
serverAvancado is CRestCRUDServerAdvanced("Cliente")

// Configura recursos
serverAvancado.EnableCache(True)
serverAvancado.RequireAuth(True)

// Registra hooks personalizados
serverAvancado.m_cHooks.RegisterHook("Cliente", "CREATE", "BEFORE", "HookClienteBeforeCreate")

// Adiciona validações customizadas
serverAvancado.m_cValidation.AddRule("Cliente", "telefone", "custom",
"Telefone deve ter pelo menos 10 dígitos",
"ValidarTelefone")

Info("✅ Servidor avançado configurado")

//----------------------------------------------------------------------
// 2. TESTAR AUTENTICAÇÃO
//----------------------------------------------------------------------
sTokenAdmin is string = serverAvancado.m_cAuth.AuthenticateUser("admin", "admin123")
sTokenGuest is string = serverAvancado.m_cAuth.AuthenticateUser("guest", "guest")

IF sTokenAdmin <> "" THEN
Info("✅ Token Admin: " + Left(sTokenAdmin, 30) + "...")
END

IF sTokenGuest <> "" THEN
Info("✅ Token Guest: " + Left(sTokenGuest, 30) + "...")
END

//----------------------------------------------------------------------
// 3. TESTAR VALIDAÇÕES
//----------------------------------------------------------------------
varCliente is Variant
varCliente.nome = "João Silva"
varCliente.email = "email_invalido"
varCliente.telefone = "123" // Muito curto

arrayErros is array of string
IF NOT serverAvancado.m_cValidation.ValidateRecord("Cliente", varCliente, arrayErros) THEN
sErros is string = "❌ Erros de validação encontrados:" + CR
FOR i = 1 TO ArrayCount(arrayErros)
sErros += "• " + arrayErros[i] + CR
END
Info(sErros)
END

//----------------------------------------------------------------------
// 4. TESTAR CACHE
//----------------------------------------------------------------------
serverAvancado.m_cCache.Set("teste_cache", "Dados de teste", 60)
varCacheResult is Variant = serverAvancado.m_cCache.Get("teste_cache")

IF VariantToJSON(varCacheResult) <> "null" THEN
Info("✅ Cache funcionando: " + varCacheResult)
END

//----------------------------------------------------------------------
// 5. INICIAR SERVIDOR
//----------------------------------------------------------------------
IF serverAvancado.StartServer(8095, "/api/v2") THEN
Info("🚀 Servidor Avançado iniciado na porta 8095" + CR + CR +
"Recursos habilitados:" + CR +

```

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Membro registado
4.618 mensagems
Publicado em agosto, 16 2025 - 3:25 AM
Última versão 6.5
——————————————————

// ============================================================================
// clsApiServerV65 – Superclasse API REST v6.5 (Hardened, rev-2)
// Gerado: 2025-08-16T01:07:05.418068Z
// ---------------------------------------------------------------------------
// Rotas admin: /api/v6/admin/openapi/import | /api/v6/admin/openapi/meta | /api/v6/admin/metrics
// Middleware: RateLimit, RequestId, Auditoria, Métricas
// Dependências: clsOpenAPI_Importer_V65, clsApiConfigV65, clsApiLoggerV65,
// clsRateLimiterV65, clsInputValidatorV65, clsMetricsV65
// ============================================================================
INCLUDE "clsOpenAPI_Importer_V65.txt"
INCLUDE "clsApiConfigV65.txt"
INCLUDE "clsApiLoggerV65.txt"
INCLUDE "clsRateLimiterV65.txt"
INCLUDE "clsInputValidatorV65.txt"
INCLUDE "clsMetricsV65.txt"

GLOBAL
gV65_AdminImportEnabled is boolean = False
gV65_AdminApiKey is string = ""

// ---------------- Inicialização ----------------
PROCEDURE Init_Server_v65()
Cfg_LoadOrDefault()
Metrics_Init()
RateLimit_Init(Cfg.rate.windowSeconds, Cfg.rate.maxRequests, Cfg.rate.burst)
Logger_Init()

// Cria tabela de auditoria se faltar
IF NOT HFileExist("AUDIT_LOG") THEN
HDescribeFile("AUDIT_LOG","AUDIT_LOG.fic","")
HDescribeItem("AUDIT_LOG","id",hItemAutoID,0,0)
HDescribeItem("AUDIT_LOG","ts",hItemDateTime,0,0)
HDescribeItem("AUDIT_LOG","ip",hItemText,64,0)
HDescribeItem("AUDIT_LOG","user",hItemText,128,0)
HDescribeItem("AUDIT_LOG","method",hItemText,8,0)
HDescribeItem("AUDIT_LOG","path",hItemText,255,0)
HDescribeItem("AUDIT_LOG","status",hItemInt,0,0)
HDescribeItem("AUDIT_LOG","latency_ms",hItemInt,0,0)
HDescribeItem("AUDIT_LOG","bytes_out",hItemInt,0,0)
HDescribeItem("AUDIT_LOG","req_id",hItemText,64,0)
HCreation("AUDIT_LOG"); HIndex("AUDIT_LOG")
END

// Registra rotas admin
WebAddRoute("/api/v6/admin/metrics", httpGet, Route_Admin_Metrics_V65)
WebAddRoute("/api/v6/admin/openapi/meta", httpGet, Route_Admin_OpenAPI_Meta_V65)
WebAddRoute("/api/v6/admin/openapi/import", httpPost, Route_Admin_OpenAPI_Import_V65)
Trace("API v6.5 (rev-2) pronta.")

PROCEDURE Enable_Admin_OpenAPI(adminKey is string)
gV65_AdminApiKey = adminKey
gV65_AdminImportEnabled = (adminKey<>"")

// -------------- Middleware utilitário --------------
PROCEDURE Middleware_Begin(out reqId is string, OUT t0 is int) : boolean
t0 = Time()
reqId = GUIDGenerate()
WebWriteHTTPHeader("X-Request-Id", reqId)

ip is string = WebserviceReadIPAddress()
apiKey is string = WebserviceReadHTTPHeader(Cfg.auth.apiKeyHeader)

IF NOT RateLimit_Check(ip, apiKey) THEN
WriteProblem(429,"Too Many Requests","Rate limit exceeded","",reqId)
AuditLog(ip,"",WebserviceReadHTTPRequestMethod(),WebserviceReadURL(),429,Time()-t0,0,reqId)
RESULT False
END
RESULT True

PROCEDURE Middleware_End(ip is string, user is string, method is string, path is string, status is int, t0 is int, bytes is int, reqId is string)
lat is int = Time()-t0
AuditLog(ip,user,method,path,status,lat,bytes,reqId)
Metrics_IncCounter("requests_total")
Metrics_Observe("request_latency_ms", lat)

// -------------- Endpoints Admin --------------
PROCEDURE Route_Admin_Metrics_V65()
ctxUser is string = WebserviceReadHTTPHeader(Cfg.auth.apiKeyHeader)
WebWriteHTTPHeader("Content-Type","application/json; charset=utf-8")
WebWriteHTTPCode(200)
WebWriteString(Metrics_ExportJSON())

PROCEDURE Route_Admin_OpenAPI_Meta_V65()
p is string = fExeDir()+"openapi_client_meta.json"
IF NOT fFileExist(p) THEN
WriteProblem(404,"Not Found","openapi_client_meta.json não encontrado","","")
RETURN
END
WebWriteHTTPHeader("Content-Type","application/json; charset=utf-8")
WebWriteHTTPCode(200); WebWriteString(fLoadText(p))

PROCEDURE Route_Admin_OpenAPI_Import_V65()
IF NOT gV65_AdminImportEnabled THEN WriteProblem(403,"Forbidden","Admin import disabled","",""); RETURN
reqId is string; t0 is int
IF NOT Middleware_Begin(reqId,t0) THEN RETURN

ip is string = WebserviceReadIPAddress()
method is string = WebserviceReadHTTPRequestMethod()
path is string = WebserviceReadURL()

adminKey is string = WebserviceReadHTTPHeader("X-API-Key")
IF adminKey="" OR adminKey<>gV65_AdminApiKey THEN
WriteProblem(403,"Forbidden","Admin API key invalid","","",reqId)
Middleware_End(ip,"",method,path,403,t0,0,reqId)
RETURN
END

raw is string = WebReadRequestBody()
IF Length(raw)>Cfg.limits.maxBodyBytes THEN
WriteProblem(413,"Payload Too Large","Body exceeds limit","","",reqId)
Middleware_End(ip,"",method,path,413,t0,0,reqId)
RETURN
END

openapiJSON is string = ""
openapiURL is string = ""

TRY
// Tenta extrair openapi_url ou openapi_json
IF Position(raw,"\"openapi_url\"")>0 THEN
v is variant
IF JSONToVariant(v, raw) AND VariantExists(v,"openapi_url") THEN openapiURL = v["openapi_url"]+""
END

IF openapiURL<>"" THEN
IF NOT IsValidURL(openapiURL) THEN
WriteProblem(400,"Bad Request","Invalid URL","","",reqId)
Middleware_End(ip,"",method,path,400,t0,0,reqId); RETURN
END
imp is clsOpenAPI_Importer_V65 = new clsOpenAPI_Importer_V65
IF imp=Null THEN WriteProblem(500,"Server Error","Allocation failed","","",reqId); RETURN END
IF NOT imp.ImportFromURL(openapiURL, fExeDir()+"api_allowed_tables.json", True) THEN
WriteProblem(500,"Import failed", imp.LastError, "", reqId)
Middleware_End(ip,"",method,path,500,t0,0,reqId); RETURN
END
ELSE
IF Position(raw,"\"openapi_json\"")>0 THEN
v2 is variant
IF JSONToVariant(v2, raw) AND VariantExists(v2,"openapi_json") THEN openapiJSON = VariantToJSON(v2["openapi_json"], taNo)
ELSE
openapiJSON = raw
END
IF openapiJSON="" THEN WriteProblem(400,"Bad Request","OpenAPI JSON not provided","","",reqId); Middleware_End(ip,"",method,path,400,t0,0,reqId); RETURN
imp2 is clsOpenAPI_Importer_V65 = new clsOpenAPI_Importer_V65
IF imp2=Null THEN WriteProblem(500,"Server Error","Allocation failed","","",reqId); RETURN END
IF NOT imp2.ImportFromJSON(openapiJSON, fExeDir()+"api_allowed_tables.json", True) THEN
WriteProblem(500,"Import failed", imp2.LastError, "", reqId)
Middleware_End(ip,"",method,path,500,t0,0,reqId); RETURN
END
END

WebWriteHTTPHeader("Content-Type","application/json; charset=utf-8")
WebWriteHTTPCode(200)
WebWriteString("{""ok"":true,""message"":""OpenAPI importado com sucesso (v6.5 rev-2).""}")
Middleware_End(ip,"",method,path,200,t0,0,reqId)

EXCEPTION
LogException("admin_import_exception", ExceptionInfo())
WriteProblem(500,"Server Error","Unhandled exception","","",reqId)
Middleware_End(ip,"",method,path,500,t0,0,reqId)
END

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Membro registado
4.618 mensagems
Publicado em agosto, 16 2025 - 3:26 AM
// ============================================================================
// clsOpenAPI_Importer_V65 – Importador universal (Swagger/OpenAPI) Hardened
// Gerado: 2025-08-16T01:07:05.418068Z
// ============================================================================
INCLUDE "clsApiConfigV65.txt"
INCLUDE "clsApiLoggerV65.txt"

STRUCT stSchemaField65
name is string
type is string
format is string
required is boolean
isFK is boolean
refTable is string
enumCSV is string
descr is string

STRUCT stTableDef65
name is string
fields is array of stSchemaField65
pkField is string
polyInfo is string

clsOpenAPI_Importer_V65 is Class
PUBLIC
LastError is string
DebugLog is array of string

PROCEDURE ImportFromURL(url is string, outWhitelistPath is string, createTables is boolean=True, defaultMethodsCSV is string="GET,POST,PATCH,DELETE") : boolean
LastError=""; DebugLog=[]
IF url="" THEN LastError="URL vazia"; RESULT False END
TRY
HTTPTimeOut(Cfg.http.timeoutMs)
IF NOT HTTPRequest(url) THEN LastError="Falha ao baixar URL: "+url; RESULT False END
r is httpResponse = HTTPGetResult(httpResult)
RESULT ImportFromJSON(r.Content, outWhitelistPath, createTables, defaultMethodsCSV)
EXCEPTION
LastError = "Exceção durante HTTPRequest: " + ExceptionInfo()
LogException("import_url_exception", LastError)
RESULT False
END

PROCEDURE ImportFromJSON(jsonText is string, outWhitelistPath is string, createTables is boolean=True, defaultMethodsCSV is string="GET,POST,PATCH,DELETE") : boolean
LastError=""; DebugLog=[]
IF jsonText="" THEN LastError="JSON vazio"; RESULT False END
IF Length(jsonText)>Cfg.limits.maxBodyBytes THEN LastError="Documento muito grande (max: "+Cfg.limits.maxBodyBytes+")"; RESULT False END

TRY
v is variant
IF NOT JSONToVariant(v, jsonText) THEN LastError="JSON inválido"; RESULT False END

// Swagger 2 → OpenAPI 3 básico
IF VariantExists(v,"swagger") THEN
conv is variant; JSONToVariant(conv,"{}")
conv["openapi"]="3.0.0"; conv["info"]=v["info"]; conv["paths"]=v["paths"]
JSONToVariant(conv["components"],"{}")
IF VariantExists(v,"definitions") THEN conv["components"]["schemas"]=v["definitions"]
v = conv; ArrayAdd(DebugLog,"Swagger 2.0 detectado – conversão aplicada.")
END

IF NOT VariantExists(v,"components") OR NOT VariantExists(v["components"],"schemas") THEN LastError="Sem components.schemas"; RESULT False
schemas is variant = v["components"]["schemas"]

tables is array of stTableDef65
FOR EACH schName OF schemas
t is stTableDef65; t.name=Lower(schName+""); t.pkField="id"; t.fields=[]
resolved is variant = _ResolveSchema(schemas[schName], schemas)
_FillTableFromSchema(resolved, t)
ArrayAdd(tables, t)
END

IF createTables THEN FOR EACH t OF tables CALL CreateTableFromDef(t) END
_GenerateWhitelist(v, tables, outWhitelistPath, defaultMethodsCSV)
_GenerateClientMeta(tables)

RESULT True
EXCEPTION
LastError = "Exceção no ImportFromJSON: " + ExceptionInfo()
LogException("import_json_exception", LastError)
RESULT False
END

PRIVATE
PROCEDURE _ResolveSchema(s is variant, allSchemas is variant) : variant
IF VariantExists(s,"$ref") THEN
ref is string = s["$ref"]+""
pos is int = Position(ref,"/schemas/")
IF pos>0 THEN
nm is string = Lower(Middle(ref, pos+9))
IF VariantExists(allSchemas,nm) THEN RESULT _ResolveSchema(allSchemas[nm], allSchemas)
END
END

out is variant; JSONToVariant(out, VariantToJSON(s, taNo))

// allOf
IF VariantExists(s,"allOf") THEN
base is variant; JSONToVariant(base,"{}")
FOR EACH part OF s["allOf"]
pr is variant = _ResolveSchema(part, allSchemas)
_MergeSchemaInto(base, pr)
END
out = base
END

// anyOf/oneOf → união de propriedades (meta polimórfica)
IF VariantExists(s,"anyOf") OR VariantExists(s,"oneOf") THEN
lst is variant = IF(VariantExists(s,"anyOf"), s["anyOf"], s["oneOf"])
base2 is variant; JSONToVariant(base2,"{}"); base2["properties"]={}; base2["required"]=[]
names is array of string
FOR EACH part OF lst
pr is variant = _ResolveSchema(part, allSchemas)
_MergeProps(base2, pr)
IF VariantExists(part,"$ref") THEN nm is string = Middle(part["$ref"]+"", Position(part["$ref"]+"","/schemas/")+9); ArrayAdd(names,nm) END
END
out = base2; out["_polyInfo"] = Join(names,"|")
END

IF VariantExists(s,"discriminator") THEN
disc is variant = s["discriminator"]
out["_polyInfo"] = "discriminator:"+IF(VariantExists(disc,"propertyName"), disc["propertyName"]+"", "")
END

RESULT out

PROCEDURE _MergeProps(INOUT base is variant, add is variant)
IF VariantExists(add,"properties") THEN
IF NOT VariantExists(base,"properties") THEN JSONToVariant(base["properties"],"{}")
FOR EACH k OF add["properties"] base["properties"][k]=add["properties"][k] END
END
IF VariantExists(add,"required") THEN
IF NOT VariantExists(base,"required") THEN JSONToVariant(base["required"],"[]")
FOR EACH rq OF add["required"]
found is boolean=False
FOR EACH x OF base["required"] IF x+""=rq+"" THEN found=True; BREAK END
IF NOT found THEN ArrayAdd(base["required"], rq+"")
END
END

PROCEDURE _MergeSchemaInto(INOUT base is variant, add is variant)
_MergeProps(base, add)

PROCEDURE _FillTableFromSchema(schema is variant, OUT t is stTableDef65)
req is array of string
IF VariantExists(schema,"required") THEN FOR EACH r OF schema["required"] ArrayAdd(req, Lower(r+"")) END
IF VariantExists(schema,"properties") THEN
FOR EACH propName OF schema["properties"]
prop is variant = schema["properties"][propName]
f is stSchemaField65
f.name = Lower(propName+"")
f.type = IF(VariantExists(prop,"type"), prop["type"]+"","string")
f.format = IF(VariantExists(prop,"format"), prop["format"]+"","")
f.required = (Lower(f.name) IN req)
f.isFK = (Right(f.name,3)="_id")
IF f.isFK THEN f.refTable = Left(f.name, Length(f.name)-3)
IF VariantExists(prop,"enum") THEN vals is array of string; FOR EACH ev OF prop["enum"] ArrayAdd(vals, ev+"") END; f.enumCSV = Join(vals,",")
IF VariantExists(prop,"description") THEN f.descr = prop["description"]+""
IF f.name="id" THEN t.pkField="id"
ArrayAdd(t.fields, f)
END
END
IF VariantExists(schema,"_polyInfo") THEN t.polyInfo = schema["_polyInfo"]+""

PROCEDURE CreateTableFromDef(t is stTableDef65)
IF HFileExist(t.name) THEN RETURN
TRY
HDescribeFile(t.name, t.name+".fic","")
hasPK is boolean=False
FOR EACH f OF t.fields
typ is int = hItemText; length is int = 255
IF f.name="id" THEN HDescribeItem(t.name,"id",hItemAutoID,0,0); hasPK=True; CONTINUE
SWITCH Lower(f.type)
CASE "integer","number":
IF Lower(f.format) IN ["int64","int32"] THEN typ=hItemInt ELSE typ=hItemReal END
CASE "boolean": typ=hItemBool
CASE "string":
IF Lower(f.format) IN ["date-time","datetime"] THEN typ=hItemDateTime
ELSEIF Lower(f.format)="date" THEN typ=hItemDate
ELSEIF Lower(f.format)="binary" THEN typ=hItemMemoBinary
ELSE typ=hItemText; length=255
CASE "array": typ=hItemText
CASE "object": typ=hItemText
OTHER CASE: typ=hItemText
END
HDescribeItem(t.name, f.name, typ, length, 0)
END
HCreation(t.name)
FOR EACH f OF t.fields IF f.isFK THEN HDescribeKey(t.name, f.name+"_idx", hUniqueFalse, f.name, hKeyNormal) END
IF NOT hasPK THEN HDescribeKey(t.name, t.pkField+"_idx", hUniqueTrue, t.pkField, hKeyNormal)
HIndex(t.name)
EXCEPTION
LastError = "Exceção na criação de tabela '"+t.name+"': "+ExceptionInfo()
LogException("table_create_exception", LastError)
END

PROCEDURE _GenerateWhitelist(v is variant, tables is array of stTableDef65, outWhitelistPath is string, defaultMethodsCSV is string)
wl is string = "{""wildcard"":false,""default"":{""methods"":[""GET""]},""tables"":{"
first is boolean=True
IF VariantExists(v,"paths") THEN
used is associative array of boolean
FOR EACH pth OF v["paths"]
base is string = _BaseResourceName(pth+"")
IF base<>"" AND NOT (Lower(base) IN used) THEN
ifN is string = IF(first,"",","); first=False
wl += ifN + "\""+Lower(base)+"\":{\"methods\":["
allowed is array of string
FOR EACH verb OF v["paths"][pth]
ver is string = Upper(verb+"")
IF ver IN ["GET","POST","PUT","PATCH","DELETE","HEAD"] THEN IF NOT (ver IN allowed) THEN ArrayAdd(allowed, ver) END
END
IF ArrayCount(allowed)=0 THEN ArrayAdd(allowed,"GET")
wl += _JoinQuoted(allowed) + "]}"
used[Lower(base)]=True
END
END
ELSE
defM is array of string = StringToArray(defaultMethodsCSV,",")
FOR EACH t OF tables
ifN2 is string = IF(first,"",","); first=False
wl += ifN2 + "\""+Lower(t.name)+"\":{\"methods\":[" + _JoinQuoted(defM) + "]}"
END
END
wl += "}}"
IF outWhitelistPath<>"" THEN fSaveText(outWhitelistPath, wl)

PROCEDURE _GenerateClientMeta(tables is array of stTableDef65)
meta is string = "{""resources"":["
first2 is boolean=True
FOR EACH t OF tables
ifM is string = IF(first2,"",","); first2=False
meta += ifM + "{""name"":"""+t.name+""",""pk"":"""+t.pkField+""",""poly"":"""+_Esc(t.polyInfo)+""",""fields"":["
i is int=1
FOR EACH f OF t.fields
meta += "{""name"":"""+f.name+""",""type"":"""+f.type+""",""format"":"""+f.format+""",""required"":"+IF(f.required,"true","false")+",""fk"":"+IF(f.isFK,"true","false")+",""ref"":"""+f.refTable+""",""enum"":"""+_Esc(f.enumCSV)+""",""descr"":"""+_Esc(f.descr)+"""}"
IF i<ArrayCount(t.fields) THEN meta+=","
i+=1
END
meta += "]}"
END
meta += "],""debug"":[]}"
fSaveText(fExeDir()+"openapi_client_meta.json", meta)

PROCEDURE _BaseResourceName(path is string) : string
parts is array of string = StringToArray(path,"/")
FOR EACH s OF parts IF s<>"" AND NOT (Left(s,1)="{") THEN RESULT Lower(s) END
RESULT ""

PROCEDURE _Esc(s is string) : string
r is string = Replace(s, "\"","\\\""); r=Replace(r, CR, "\\n"); RESULT r

PROCEDURE _JoinQuoted(a is array of string) : string
IF ArrayCount(a)=0 THEN RESULT "" ELSE
s is string=""; i is int=1
FOR EACH x OF a s+="\""+x+"\"" + IF(i<ArrayCount(a),",",""); i+=1 END
RESULT s

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Membro registado
4.618 mensagems
Publicado em agosto, 16 2025 - 3:26 AM
// ============================================================================
// clsApiConfigV65 – Config externa (api_config_v65.json)
// Gerado: 2025-08-16T01:07:05.418068Z
// ============================================================================
STRUCT stCfgAuth
apiKeyHeader is string
END
STRUCT stCfgHttp
timeoutMs is int
END
STRUCT stCfgRate
windowSeconds is int
maxRequests is int
burst is int
END
STRUCT stCfgLimits
maxBodyBytes is int
maxSelectLen is int
maxWhereLen is int
END
STRUCT stCfg
auth is stCfgAuth
http is stCfgHttp
rate is stCfgRate
limits is stCfgLimits

GLOBAL
Cfg is stCfg

PROCEDURE Cfg_LoadOrDefault()
Cfg.auth.apiKeyHeader = "X-API-Key"
Cfg.http.timeoutMs = 60000
Cfg.rate.windowSeconds= 60
Cfg.rate.maxRequests = 300
Cfg.rate.burst = 60
Cfg.limits.maxBodyBytes = 4_000_000
Cfg.limits.maxSelectLen = 256
Cfg.limits.maxWhereLen = 1024

p is string = fExeDir()+"api_config_v65.json"
IF fFileExist(p) THEN
v is variant
IF JSONToVariant(v, fLoadText(p)) THEN
IF VariantExists(v,"auth") AND VariantExists(v["auth"],"apiKeyHeader") THEN Cfg.auth.apiKeyHeader = v["auth"]["apiKeyHeader"]+""
IF VariantExists(v,"http") AND VariantExists(v["http"],"timeoutMs") THEN Cfg.http.timeoutMs = v["http"]["timeoutMs"]
IF VariantExists(v,"rate") THEN
IF VariantExists(v["rate"],"windowSeconds") THEN Cfg.rate.windowSeconds = v["rate"]["windowSeconds"]
IF VariantExists(v["rate"],"maxRequests") THEN Cfg.rate.maxRequests = v["rate"]["maxRequests"]
IF VariantExists(v["rate"],"burst") THEN Cfg.rate.burst = v["rate"]["burst"]
END
IF VariantExists(v,"limits") THEN
IF VariantExists(v["limits"],"maxBodyBytes") THEN Cfg.limits.maxBodyBytes = v["limits"]["maxBodyBytes"]
IF VariantExists(v["limits"],"maxSelectLen") THEN Cfg.limits.maxSelectLen = v["limits"]["maxSelectLen"]
IF VariantExists(v["limits"],"maxWhereLen") THEN Cfg.limits.maxWhereLen = v["limits"]["maxWhereLen"]
END
END
ELSE
json is string = "{""auth"":{""apiKeyHeader"":"""+Cfg.auth.apiKeyHeader+"""},""http"":{""timeoutMs"":"+Cfg.http.timeoutMs+"},""rate"":{""windowSeconds"":"+Cfg.rate.windowSeconds+",""maxRequests"":"+Cfg.rate.maxRequests+",""burst"":"+Cfg.rate.burst+"},""limits"":{""maxBodyBytes"":"+Cfg.limits.maxBodyBytes+",""maxSelectLen"":"+Cfg.limits.maxSelectLen+",""maxWhereLen"":"+Cfg.limits.maxWhereLen+"}}"
fSaveText(p, json)
END

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Membro registado
4.618 mensagems
Publicado em agosto, 16 2025 - 3:27 AM
// ============================================================================
// clsApiLoggerV65 – Logging & Auditoria (HFSQL + Trace JSON)
// Gerado: 2025-08-16T01:07:05.418068Z
// ============================================================================
PROCEDURE Logger_Init()
Trace("Logger v65 init")

PROCEDURE LogJSON(level is string, event is string, ctx is variant)
msg is string = "{""ts"":"""+DateToString(Today(),"YYYY-MM-DD")+"T"+TimeToString(Time(),"HHMMSS")+"Z" + """,""level"":"""+level+""",""event"":"""+event+""",""ctx"":"+VariantToJSON(ctx, taNo)+"}"
Trace(msg)

PROCEDURE LogInfo(event is string, ctx is variant)
LogJSON("info", event, ctx)

PROCEDURE LogError(event is string, ctx is variant)
LogJSON("error", event, ctx)

PROCEDURE LogException(event is string, detail is string)
loc is variant; JSONToVariant(loc, "{}"); loc["detail"]=detail
LogJSON("exception", event, loc)

PROCEDURE AuditLog(ip is string, user is string, method is string, path is string, status is int, latency is int, bytesOut is int, reqId is string)
AUDIT_LOG.ts = Now()
AUDIT_LOG.ip = ip
AUDIT_LOG.user = user
AUDIT_LOG.method = method
AUDIT_LOG.path = path
AUDIT_LOG.status = status
AUDIT_LOG.latency_ms = latency
AUDIT_LOG.bytes_out = bytesOut
AUDIT_LOG.req_id = reqId
HAdd(AUDIT_LOG)

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Membro registado
4.618 mensagems
Publicado em agosto, 16 2025 - 3:27 AM
// ============================================================================
// clsRateLimiterV65 – Rate limiting simples (IP + APIKey)
// Gerado: 2025-08-16T01:07:05.418068Z
// ============================================================================
GLOBAL
RL_Store is associative array of string // key -> "count|windowStartMs"

PROCEDURE RateLimit_Init(windowSec is int, maxReq is int, burst is int)
// parâmetros já carregados de Cfg; store inicia vazio

PROCEDURE RateLimit_Check(ip is string, apiKey is string) : boolean
key is string = Lower(ip+"|"+apiKey)
now is int = TimeToInteger(Time())
win is int = Cfg.rate.windowSeconds * 100 // centésimos
entry is string = RL_Store[key]
IF entry="" THEN RL_Store[key] = "1|"+now; RESULT True
parts is array of string = StringToArray(entry, "|")
cnt is int = Val(parts[1]); start is int = Val(parts[2])
IF (now - start) > win THEN
RL_Store[key] = "1|"+now
RESULT True
END
cnt += 1
IF cnt > Cfg.rate.maxRequests THEN RESULT False
RL_Store[key] = NumToString(cnt)+"|"+start
RESULT True

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Membro registado
4.618 mensagems
Publicado em agosto, 16 2025 - 3:28 AM
// ============================================================================
// clsInputValidatorV65 – validação e sanitização de entradas
// Gerado: 2025-08-16T01:07:05.418068Z
// ============================================================================
PROCEDURE IsValidURL(u is string) : boolean
RESULT (Position(Lower(u), "http://")=1 OR Position(Lower(u), "https://")=1)

PROCEDURE ValidateOpenAPIEnvelope(jsonText is string) : boolean
v is variant
IF NOT JSONToVariant(v, jsonText) THEN RESULT False
IF VariantExists(v, "openapi") OR VariantExists(v, "swagger") THEN RESULT True
RESULT (VariantExists(v, "components") AND VariantExists(v["components"], "schemas"))

PROCEDURE SanitizeQueryParam(val is string, maxLen is int) : string
s is string = Left(val, maxLen)
s = Replace(s, ";", "")
s = Replace(s, "--", "")
s = Replace(s, "/*", "")
s = Replace(s, "*/", "")
RESULT s

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Membro registado
4.618 mensagems
Publicado em agosto, 16 2025 - 3:28 AM
// ============================================================================
// clsMetricsV65 – métricas simples JSON
// Gerado: 2025-08-16T01:07:05.418068Z
// ============================================================================
GLOBAL
MET_Counters is associative array of int
MET_LastLatencyMs is int = 0

PROCEDURE Metrics_Init()
MET_Counters = []

PROCEDURE Metrics_IncCounter(name is string)
MET_Counters[name] += 1

PROCEDURE Metrics_Observe(name is string, val is int)
IF name="request_latency_ms" THEN MET_LastLatencyMs = val

PROCEDURE Metrics_ExportJSON() : string
s is string = "{""counters"":{"
i is int = 0
FOR EACH k OF MET_Counters
IF i>0 THEN s+=","
s += "\""+k+"\": "+MET_Counters[k]
i+=1
END
s += "},""last_latency_ms"": "+MET_LastLatencyMs+"}"
RESULT s

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Membro registado
4.618 mensagems
Publicado em agosto, 16 2025 - 3:29 AM
// ============================================================================
// clsApiClientV65 – Client Hardened v6.5 (rev-2)
// Gerado: 2025-08-16T01:07:05.418068Z
// ============================================================================
STRUCT stClientResponseV65
status is int
body is string

clsApiClientV65 is Class
PRIVATE
_baseURL is string
_apiKey is string
_apiKeyHdr is string = "X-API-Key"
_bearer is string
_adminKey is string

PUBLIC
PROCEDURE Constructor(base is string="")
_baseURL = base

PROCEDURE SetBaseURL(u is string)
_baseURL = u

PROCEDURE SetAuthApiKey(k is string, header is string="X-API-Key")
_apiKey = k
_apiKeyHdr = header

PROCEDURE SetAuthBearer(tok is string)
_bearer = tok

PROCEDURE SetAdminKey(k is string)
_adminKey = k

PROCEDURE _AddStdHeaders(isAdmin is boolean=False, contentType is string="application/json", accept is string="application/json")
HTTPClearHeader()
IF _apiKey<>"" THEN HTTPAddHeader(_apiKeyHdr, _apiKey)
IF _bearer<>"" THEN HTTPAddHeader("Authorization", "Bearer "+_bearer)
IF isAdmin AND _adminKey<>"" THEN HTTPAddHeader("X-API-Key", _adminKey)
IF contentType<>"" THEN HTTPAddHeader("Content-Type", contentType)
IF accept<>"" THEN HTTPAddHeader("Accept", accept)

PROCEDURE _Http(method is string, path is string, body is string="", isAdmin is boolean=False, contentType is string="application/json") : stClientResponseV65
res is stClientResponseV65
IF _baseURL="" THEN res.status=0; res.body="BaseURL não definida"; RESULT res END

fullURL is string = _baseURL + path
_AddStdHeaders(isAdmin, contentType)

reqType is int = httpGet
m is string = Upper(method)
SWITCH m
CASE "GET": reqType = httpGet
CASE "POST": reqType = httpPost
CASE "PUT": reqType = httpPut
CASE "DELETE": reqType = httpDelete
CASE "PATCH": reqType = httpPost; HTTPAddHeader("X-HTTP-Method-Override","PATCH")
CASE "HEAD": reqType = httpHead
OTHER CASE: reqType = httpGet
END

TRY
ok is boolean
IF m="GET" OR m="HEAD" THEN ok = HTTPRequest(fullURL, reqType) ELSE ok = HTTPRequest(fullURL, reqType, body) END
IF NOT ok THEN res.status = 599; res.body = "Falha HTTPRequest: "+ErrorInfo(errMessage); RESULT res END
httpRes is httpResponse = HTTPGetResult(httpResult)
res.status = httpRes.StatusCode
res.body = httpRes.Content
RESULT res
EXCEPTION
res.status = 599
res.body = "Exceção HTTP: "+ExceptionInfo()
RESULT res
END

// -------- Admin --------
PROCEDURE AdminImportOpenAPI(urlOrJSON is string) : stClientResponseV65
IF Position(Lower(urlOrJSON),"http://")=1 OR Position(Lower(urlOrJSON),"https://")=1 THEN
body is string = "{""openapi_url"":"""+urlOrJSON+"""}"
RESULT _Http("POST", "/api/v6/admin/openapi/import", body, True)
ELSE
body is string = "{""openapi_json"":"+urlOrJSON+"}"
RESULT _Http("POST", "/api/v6/admin/openapi/import", body, True)
END

PROCEDURE GetOpenAPIMeta() : stClientResponseV65
RESULT _Http("GET", "/api/v6/admin/openapi/meta", "", False)

PROCEDURE GetMetrics() : stClientResponseV65
RESULT _Http("GET", "/api/v6/admin/metrics", "", False)

PROCEDURE OpenSwagger()
IF _baseURL<>"" THEN ShellExecute(_baseURL+"/api/v6/docs")

PROCEDURE SelfTest() : string
r is stClientResponseV65 = _Http("GET","/api/v6/version")
RESULT r.status+" -> "+r.body

// -------- Query Builder --------
PROCEDURE Table(resName is string) : clsApiQueryV65
q is clsApiQueryV65
q._client = Self
q._resource = Lower(resName)
RESULT q

// -------- Files (BLOB) --------
PROCEDURE UploadFile(localPath is string, table is string, recordID is string, field is string) : stClientResponseV65
IF NOT fFileExist(localPath) THEN RESULT {"status"=0,"body"="Arquivo não encontrado: "+localPath}
buf is Buffer = fLoadBuffer(localPath)
b64 is string = BufferToBase64(buf)
mimetype is string = _MimeFromExt(ExtractString(localPath, ".",-1))
payload is string = "{""table"":"""+table+""",""record_id"":"""+recordID+""",""field"":"""+field+""",""filename"":"""+fExtractPath(localPath, fFile+fExtension)+""",""mimetype"":"""+mimetype+""",""content_base64"":"""+b64+"""}"
RESULT _Http("POST","/api/v6/files", payload, False)

PROCEDURE DownloadFileById(fileID is string, saveAsPath is string) : stClientResponseV65
r is stClientResponseV65 = _Http("GET","/api/v6/files/"+fileID, "", False, "")
IF r.status=200 THEN
fSaveBuffer(saveAsPath, HTTPGetResult(httpResponse).Content)
END
RESULT r

PROCEDURE _MimeFromExt(ext is string) : string
e is string = Lower(ext)
SWITCH e
CASE "pdf": RESULT "application/pdf"
CASE "png": RESULT "image/png"
CASE "jpg","jpeg": RESULT "image/jpeg"
CASE "gif": RESULT "image/gif"
CASE "svg": RESULT "image/svg+xml"
CASE "bmp": RESULT "image/bmp"
CASE "doc","docx": RESULT "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
CASE "xls","xlsx": RESULT "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
CASE "csv": RESULT "text/csv"
DEFAULT: RESULT "application/octet-stream"
END

// ---------------- Query Builder ----------------
clsApiQueryV65 is Class
PRIVATE
_client is clsApiClientV65
_resource is string
_select is string
_where is string
_order is string
_limit is int = 0
_offset is int = 0
PUBLIC
PROCEDURE Select(cols is string) : clsApiQueryV65
_select = cols; RESULT Self

PROCEDURE Where(expr is string) : clsApiQueryV65
_where = expr; RESULT Self

PROCEDURE Order(expr is string) : clsApiQueryV65
_order = expr; RESULT Self

PROCEDURE Limit(n is int) : clsApiQueryV65
_limit = n; RESULT Self

PROCEDURE Offset(n is int) : clsApiQueryV65
_offset = n; RESULT Self

PROCEDURE _QS() : string
qs is string = ""
IF _select<>"" THEN qs += IF(qs="","?","&")+"select="+URLEncode(_select)
IF _where<>"" THEN qs += IF(qs="","?","&")+"where="+URLEncode(_where)
IF _order<>"" THEN qs += IF(qs="","?","&")+"order="+URLEncode(_order)
IF _limit>0 THEN qs += IF(qs="","?","&")+"limit="+_limit
IF _offset>0 THEN qs += IF(qs="","?","&")+"offset="+_offset
RESULT qs

PROCEDURE List() : stClientResponseV65
RESULT _client._Http("GET", "/api/v6/"+_resource+_QS(), "", False)

PROCEDURE Get(id is string) : stClientResponseV65
RESULT _client._Http("GET", "/api/v6/"+_resource+"/"+id, "", False)

PROCEDURE Count() : stClientResponseV65
oldSel is string = _select
_select = ""
r is stClientResponseV65 = _client._Http("GET","/api/v6/"+_resource+"/count"+_QS(), "", False)
_select = oldSel
RESULT r

PROCEDURE ExportCSV() : stClientResponseV65
RESULT _client._Http("GET","/api/v6/"+_resource+"/export.csv"+_QS(), "", False, "")

PROCEDURE InsertVariant(v is variant) : stClientResponseV65
body is string = VariantToJSON(v, taNo)
RESULT _client._Http("POST","/api/v6/"+_resource, body, False)

PROCEDURE Insert(jsonBody is string) : stClientResponseV65
RESULT _client._Http("POST","/api/v6/"+_resource, jsonBody, False)

PROCEDURE UpdateVariant(id is string, v is variant) : stClientResponseV65
body is string = VariantToJSON(v, taNo)
RESULT _client._Http("PATCH","/api/v6/"+_resource+"/"+id, body, False)

PROCEDURE Update(id is string, jsonBody is string) : stClientResponseV65
RESULT _client._Http("PATCH","/api/v6/"+_resource+"/"+id, jsonBody, False)

PROCEDURE Delete(id is string) : stClientResponseV65
RESULT _client._Http("DELETE","/api/v6/"+_resource+"/"+id, "", False)

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Membro registado
4.618 mensagems
Publicado em agosto, 16 2025 - 3:29 AM
# Client v6.5 (rev-2) – QuickStart
```wl
c is clsApiClientV65("http://localhost:8080")
c.SetAuthApiKey("SUA_CHAVE")
c.SetAdminKey("SUA_CHAVE_ADMIN_FORTE")

// Importar OpenAPI por URL
Trace(c.AdminImportOpenAPI("https://exemplo.com/openapi.json").status)

// Criar pedido
p is variant
p.cliente_id=1; p.data="2025-08-15T12:00:00"; p.status="NOVO"; p.valor_total=0
Trace(c.Table("pedidos").InsertVariant(p).body)

// Listar
Trace(c.Table("pedidos").Select("id,data,status").Order("data,-id").Limit(20).List().body)
```

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Membro registado
4.618 mensagems
Publicado em agosto, 16 2025 - 3:31 AM
{
"info": {
"name": "API v6.5 \u2013 Demo",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "SelfTest",
"request": {
"method": "GET",
"header": [
{
"key": "X-API-Key",
"value": "{{apiKey}}"
}
],
"url": {
"raw": "{{baseURL}}/api/v6/version",
"host": [
"{{baseURL}}"
],
"path": [
"api",
"v6",
"version"
]
}
}
},
{
"name": "Admin Import (JSON)",
"request": {
"method": "POST",
"header": [
{
"key": "X-API-Key",
"value": "{{adminKey}}"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\"openapi_json\": {\"openapi\":\"3.0.0\",\"info\":{\"title\":\"demo\",\"version\":\"1\"}}}"
},
"url": {
"raw": "{{baseURL}}/api/v6/admin/openapi/import",
"host": [
"{{baseURL}}"
],
"path": [
"api",
"v6",
"admin",
"openapi",
"import"
]
}
}
}
],
"variable": [
{
"key": "baseURL",
"value": "http://localhost:8080"
},
{
"key": "apiKey",
"value": "SUA_CHAVE"
},
{
"key": "adminKey",
"value": "SUA_CHAVE_ADMIN_FORTE"
}
]
}

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/
Membro registado
4.618 mensagems
Publicado em agosto, 16 2025 - 3:31 AM
{
"baseURL": "http://localhost:8080",
"apiKey": "SUA_CHAVE",
"adminKey": "SUA_CHAVE_ADMIN_FORTE",
"bearer": "SEU_TOKEN_JWT"
}

--
Adriano José Boller
______________________________________________
Consultor e Representante Oficial da
PcSoft no Brasil
+55 (41) 99949 1800
adrianoboller@gmail.com
skype: adrianoboller
http://wxinformatica.com.br/