|
| 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()
📚 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()
✅ 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/ |
| |
| |
| | | |
|
| | | | |
| | |
|