DEV Community

Josimar Junior
Josimar Junior

Posted on

Um microblog usando Protheus - Rest Server, parte 5, GET e query parameters

Introdução

Seguindo com a exploração dos modelos para construção dos métodos de apis utilizando Advpl no Rest Protheus, nesta publicação será abordado como fazer uso dos recursos de query parameters e assim permitir pesquisa e exploração das informações no ERP.

Na publicação anterior os métodos POST na uri /perfis e GET, DELETE e PUT na uri /perfis/:id foram modificados para utilizar o modelo de dados do MVC Advpl como camada de interação com o banco de dados, o link para a publicação anterior está aqui.

Entretanto um dos endpoints muito usado até então não foi modificado, o GET na uri /perfis. Este é o endpoint onde é conseguida a lista dos recursos oferecidos pela api. Essa modificação não aconteceu no passo anterior, pois não é possível realizar a recuperação de uma lista usando um modelo de dados que foi desenvolvido para lidar com um único registro.

Em função disso, será mostrada uma alternativa de como implementar de forma simplificada:

  • filtros utilizando expressões SQL limitadas a partir de dados informados na api;
  • restrição das propriedades a serem retornadas, reduzindo o tamanho da resposta;
  • paginação dos dados da api, essencial para avaliação de quantidade de itens e;
  • definição da ordem dos dados na requisição.

Deve ser ressaltado que a escolha de demonstrar a implementação desses recursos não assegura que o código resultante esteja pronto para apis que tenham necessidades avançadas, seja de desempenho, seja de complexidade da lógica de negócio envolvida. O código construído tem o objetivo de ser claro e mostrar exatamente onde as regras são extraídas e como são transformadas em cada um dos recursos.

Por que desenvolver estes recursos?

Quando endpoints que oferecem a possibilidade de listagem dos itens não possuem flexibilidade de filtro ou pesquisa, é bem ruim ser um cliente dessa api, afinal todo o trabalho manipulação dos dados para exibição fica como responsabilidade do cliente, portanto, oferecer recursos para filtrar e controlar quais e como as propriedades devem ser retornadas é essencial para ser uma boa api.

A Totvs possui um guia de implementação de apis para ser utilizado pelos times internamente e especificamente sobre retorno de coleções pode ser lido e avaliado aqui. O guia não será seguido nesta série apesar de várias semelhanças poderem ser identificadas.

A implementação

Os recursos mencionados e criados para permitir a manipulação do retorno da coleção de GET /perfis/ foram:

  • paginação: permitir que uma quantidade limitada de perfis sejam retornados por requisição no endpoint, o principal benefício é que um cliente consegue então exibir uma lista menor mais rápido e caso precise carregue mais itens com uma quantidade maior depois;
  • ordenação: conseguir pelo cliente indicar a ordem que os resultados devem ser retornados, um exemplo é a ordenação por nome ou por data de criação que são usadas por razões diferentes no cliente, deixar a ordem fixa poderia atender somente uma das formas de uso;
  • projeção de retorno: quando necessário os valores de somente uma ou duas propriedades de um certo endpoint, recuperar todas as propriedades e usar somente o que precisava é desperdício de recurso;
  • filtro: este é um aspecto delicado, enquanto é um excelente recurso e provavelmente o mais necessário, oferecer essa possibilidade implica em cuidado extra para evitar códigos maliciosos serem injetados no programa durante a execução.
  • contagem: quando filtro é permitido no endpoint, a contagem passa a ter complicações para execução, pois o filtro precisa ser considerado também. O principal objetivo da contagem é indicar que existem ou não mais itens para carregar pelo cliente.

A seguir os detalhes da implementação dos recursos, de forma simplificada mostrando como que o Rest Protheus oferece e como fazer em Advpl.

A base

Para a construção destes recursos a primeira alteração foi trocar a iteração sobre a tabela ZT0 com while e DbSkip para a execução de uma consulta SQL na recuperação dos perfis a serem retornados. O ganho com esta simples alteração é a possibilidade de construção dinâmica da query, com os cuidados especiais para não causar quebras por entradas incorretas informadas na requisição.

A implementação mostrada ainda pode ter quebras em condições bem específicas de uso, então se tem a intenção de usar como base para sua api, teste bem!

oPrepStat := FwPreparedStatement():New(cDataQuery)
...
oPrepStat:SetLike(nI, aQryValues[nI, QRY_VAL_VALUE])
...
oPrepStat:SetString(nI, cTemp)
...
cDataQuery := oPrepStat:getFixQuery()
cTempAlias := GetNextAlias()
DbUseArea(.T., "TOPCONN", TcGenQry(,, cDataQuery), cTempAlias, .F., .F.)
Enter fullscreen mode Exit fullscreen mode

Acima a classe FwPreparedStatement é usada para ajudar na construção da consulta SQL, essa classe colabora com a avaliação de instruções maliciosas que vieram pelos query parameters com as chaves que irão tornar-se componentes na consulta.
Uma opção a FwPreparedStatement é a função TcGenQry2.

A outra alteração que serviu de base para a implementação foi a utilização de um mapa entre os campos na tabela e as propriedades retornadas pelo endpoint. O ganho com isso é a facilidade para descobrir o campo que certa propriedade se refere, assim os recursos são implementados de forma mais "simplificada".

// Mapeia os campos da query com as propriedades
aFieldsMap := {;
    {"email", "ZT0_EMAIL", "C", "ZT0_EMAIL"},;
    {"user_id", "ZT0_USRID", "C", "ZT0_USRID"},;
    {"name", "ZT0_NOME", "C", "ZT0_NOME"},;
    {"admin", "ZT0_ADMIN", "L", "ZT0_ADMIN"},;
    {"inserted_at", "ZT0_INS_AT", "D", "I_N_S_D_T_"},;
    {"updated_at", "ZT0_UPD_AT", "D", "S_T_A_M_P_"} ;
}
Enter fullscreen mode Exit fullscreen mode

As propriedades usadas

A lista dos componentes de query parameters utilizados para permitir essas operações é apresentada a seguir.

  • page
  • pagesize
  • order
  • fields
  • aQueryString

A paginação

  • page e pagesize: a combinação que permite receber ?page=1&pagesize=10 e construir um retorno com os primeiros 10 itens da 1a página e também permite a requisição como ?page=11&pagesize=6 em que 11a página será exibida com 6 itens. Isso é relevante demais para páginas web ou aplicativos que no carregamento inicial não precisam de um volume grande de itens para exibição. O conceito de paginação por completo precisa ainda de uma indicação da existência de mais itens e isso é atendido com a contagem.

Os trechos de código destacados para paginação estão a seguir.

wsreceive page, pageSize
...
default self:page := 1
default self:pageSize := 10
...
// montagem da paginação
nItemFrom := (self:page - 1) * self:pageSize + 1
nItemTo := (self:page) * self:pageSize
...
cProjQuery += "ROW_NUMBER() OVER (order by "+ xyz +") SEQITEM "
...
cDataQuery += "where SEQITEM >= "+ cValToChar(nItemFrom) +" and SEQITEM <= "+ cValToChar(nItemTo) +" "
Enter fullscreen mode Exit fullscreen mode

Em resumo, (1) receber os query parameters, (2) garantir valor padrão e então (3) utilizar o recurso ROW_NUMBER() OVER na consulta SQL para a recuperação dos registros e a marcação de sequência que posteriormente já chega filtrada na camada Advpl. Sem necessidade de if ou incrementos no Advpl.

A ordenação

  • order: a chave que permite indicar qual a ordem que o retorno dos dados deve seguir, receber a instrução ?order=name indica que ordenar pela propriedade name, independente do que esta propriedade signifique internamente no endpoint e receber ?order=-inserted_at,name indica retornar por ordem inversa de criação (sendo os mais recentes primeiro) e ordem alfabética dos nomes.

Os trechos de código a serem destacados sobre ordenação estão a seguir.

wsreceive order
...
default self:order := ""
...
// montagem da ordem
if !Empty(Alltrim(self:order))
        aTemp := StrTokArr(self:order, ",")
        nMax := Len(aTemp)

        cOrderBy := ""
        for nI := 1 to nMax
        // um monte de código para identificar os campos e ordenação
        next nI
        // Remove a última vírgula (,)
        cOrderBy := SubStr(cOrderBy, 1, Len(cOrderBy)-1)
    endif

if Empty(cOrderBy)
    cOrderBy := "ZT0_NOME asc"
endif
...
cProjQuery += "ROW_NUMBER() OVER (order by "+ cOrderBy +") SEQITEM "
Enter fullscreen mode Exit fullscreen mode

Este é o primeiro dos recursos permitidos que exige algoritmo para identificar a lista das propriedades e os campos que precisarão ser utilizados neste ordenação, receber ?order=-age,name significa reconhecer os campos ou expressões associados com estas propriedades e neste caso, usando o mapa mencionado anteriormente. Por último usar esta expressão na consulta SQL e assim garantir a ordenação em conjunto com a marcação e sequenciamento dos registros pela função ROW_NUMBER() OVER(order) nome_da_coluna.

Alguns bancos de dados podem não suportar a ordenação em conjunto com a paginação, vale estudar o banco de dados e versão utilizada para garantir o máximo possível ainda em nível de banco de dados e não na camada Advpl.

A projeção

  • fields: a chave por onde são indicadas as propriedades que devem ser retornadas do recurso pesquisado, um exemplo é retornar somente email e name quando recebido ?fields=email,name. Isso facilita os inúmeros casos quando poucas informações são exigidas em uma requisição, mas o item possui outras várias propriedades. Esta é mais uma das propriedades que utiliza do mapa de propriedades e campos como referência para sua execução.

Os trechos para destaque da projeção de propriedades da api estão a seguir.

wsreceive fields
...
default self:fields := ""
...
// monta as propriedades escolhidas para retorno
aRetProps := StrTokArr(self:fields, ",")
nRetProps := Len(aRetProps)

if nRetProps == 0
    aRetProps := {"email", "user_id", "name", "admin", "inserted_at", "updated_at"}
    nRetProps := Len(aRetProps)
endif
...
jTempItem := aTail(jResponse["items"])

for nI := 1 to nRetProps
    // recupera o nome da propriedade
    cTemp := aRetProps[nI]

    // recupera o mapa propriedade x campo
    nTemp := aScan(aFieldsMap, {|x| x[MAP_PROP] == cTemp})

    if nTemp > 0
        // recupera o valor para a propriedade
        xPropValue := (cTempAlias)->(FieldGet(FieldPos(aFieldsMap[nTemp, MAP_FIELD])))

        // atribui o valor na propriedade
        if aFieldsMap[nTemp, MAP_TYPE] == "C"
            jTempItem[cTemp] := RTrim(xPropValue)
        elseif aFieldsMap[nTemp, MAP_TYPE] == "L"
            jTempItem[cTemp] := xPropValue == "1"
        else
            jTempItem[cTemp] := xPropValue
        endif
    endif
next nI
Enter fullscreen mode Exit fullscreen mode

Os passos na implementação são (1) recuperar as propriedades indicadas para retorno, (2) transformar em lista usando o mapa, com o cuidado de limitar as propriedades de retorno apropriadas e (3) montar a resposta considerando o recebido na requisição.

A montagem do retorno está longe de ter o código mais apropriado e genérico, contudo dá uma excelente indicação do quão complicado é lidar com tipos e expressões retornadas da tabela para a api.

O filtro

  • propriedade=valor: este é o recurso que não está associado com uma chave específica. Filtros avançados e com expressões complexas estão associados com o parâmetro ?filter=expressão, contudo a construção de um parser para expressões e complicada demais para ser simplificada e demonstrada nessa série, em função disso o caminho para mostrar filtros de uma forma que funcione minimamente são os filtros como este, ?admin=true onde indica os perfis de administradores, ou ?name=a&admin=true em que montaria uma condição SQL similar à ZT0_NOME like '%a%' and ZT0_ADMIN = '1' e retornaria os perfis com a letra a minúscula no nome e são administradores.

Com os exemplos indicados é possível perceber que usando o mapa de propriedades x campos existe a chance de trocar o tipo de operador conforme o tipo ou nome do campo/propriedade ou qualquer outra lógica, a decisão no código de exemplo foi usar a comparação com o operador like para caracteres, a operação = '1' para a propriedade admin e a operação = para os demais campos. Qualquer comparação com operadores diferentes exigiria a implementação de mais complexidade na montagem da expressão a ser recebida na requisição para o filtro e consequentemente mais complexidade para identificar o que fazer e montar a consulta SQL.

Os trechos importantes para a implementação são apresentados a seguir.

// monta a condição para a query
//  suporta filtros simples com operador LIKE -> campo like %valor%
cCondition := "ZT0.D_E_L_E_T_ = ' ' "
aQryValues := {}
for nI := 1 To Len(self:aQueryString)
    cTempField := Lower(self:aQueryString[nI, QRY_PARAM_KEY])
    nTemp := aScan(aFieldsMap, {|x| x[MAP_PROP] == cTempField})
    // quando encontra cria a expressão e guarda o valor para atribuir
    if nTemp > 0
        if aFieldsMap[nTemp, MAP_TYPE] == "C"
            cCondition += "and ZT0." + aFieldsMap[nTemp, MAP_FIELD] + " like ? "
        else
            cCondition += "and ZT0." + aFieldsMap[nTemp, MAP_FIELD] + " = ? "
        endif
        // mantem par com tipo [QRY_VAL_TYPE] e valor [QRY_VAL_VALUE]
        aAdd(aQryValues, {aFieldsMap[nTemp, MAP_TYPE], self:aQueryString[nI, QRY_PARAM_VALUE]} )
    endif
next nI
...
// tabela e condições
cSubQuery := "from " + RetSqlName("ZT0") + " ZT0 "
cSubQuery += "where " + cCondition
...
// query para os dados
cDataQuery := "select * "
cDataQuery += "from ( " + cProjQuery + cSubQuery + " ) QUERYDATA "
cDataQuery += "where SEQITEM >= "+ cValToChar(nItemFrom) +" and SEQITEM <= "+ cValToChar(nItemTo) +" "
Enter fullscreen mode Exit fullscreen mode

Os passos para realizar o filtro são os mais complexos, envolve (1) percorrer todas as propriedades recebidas por query parameters, (2) identificar os componentes que são propriedades válidas para o filtro (mais uma vez o mapa em ação), (3) verificar qual operador utilizar conforme propriedade e campo na consulta SQL, (4) marcar o valor para ser preenchido com ? pois depois será substituído pelo valor com uso da classe FwPreparedStatement, (4) substituir o valor e finalmente (5) montar e executar a query com a condição interferindo no resultado da paginação (expressão ROW_NUMBER OVER()).
Essa dificuldade é paga quando o retorno da api é minimizado e quando a aplicação cliente não precisa aplicar uma preparação pesada para lidar com os dados!

A contagem

O único dos recursos que não é um pedido do cliente e sim um retorno adicional para a paginação. Sem a indicação da quantidade de registros totais, a aplicação cliente precisaria fazer uma requisição e não ter dado retornado para saber que "acabou" e não há mais dados. Com a indicação da quantidade total com alguma propriedade como total, size, quantity, etc é possível informar ao cliente o número de registros e evitar uma última requisição sem dado para retornar.

Um modelo alternativo é o seguido pelo guia de api da Totvs com o uso da propriedade has_next, com detalhes neste link. O benefício com esta propriedade é indicar de forma simplificada para o cliente a existência de mais registros para a busca, o contra-ponto é não conseguir indicar o total de registros só com ela.

Os detalhes da implementação deste retorno estão a seguir.

// Recupera a quantidade de registros
cCountQuery := "select count(*) ROWS_QT " + cSubQuery
...
oPrepStat := FwPreparedStatement():New(cCountQuery)
for nI := 1 to Len(aQryValues)
// lógica para preencher as marcações com ? da query
next nI

cCountQuery := oPrepStat:getFixQuery()
DbUseArea(.T., "TOPCONN", TcGenQry(,, cCountQuery), cTempAlias, .F., .F.)

if (cTempAlias)->(EOF())
    nCount := 0
else
    nCount := (cTempAlias)->ROWS_QT
endif
jResponse["size"] := nCount
Enter fullscreen mode Exit fullscreen mode

Depois de entender como funciona a consulta SQL para a recuperação dos dados, não há segredo na consulta SQL de fazer a contagem, afinal única alteração que acontece é a substituição da lista de campos da expressão select por count(*).

Conclusão

Esta é a primeira publicação da série que apresenta alta quantidade de código, conceitos e também o uso das propriedades da classe e métodos do rest com Advpl. Tudo isso implica em conteúdo denso para assimilar e entender com profundidade.

O código apresentado tem muito a ser melhorado e pode ser utilizado em uma api em produção com as melhorias apropriadas para casos específicos. Um código similar ao mostrado nesta publicação foi base para a implementação da classe FwAdapterBaseV2, que possui estes conceitos encapsulados e pode ser usada na construção de apis. A próxima publicação será uma terceira versão desse endpoint de listagem usando a classe FwAdapterBaseV2.

O código a seguir mostra a implementação completa de método de listagem, que também pode ser visualizado no pull request no GitHub.

wsmethod GET V2ALL wsreceive page, pageSize, order, fields wsservice Perfis
    local lProcessed    as logical
    local jResponse     as object
    local jTempItem     as object
    local cTempAlias    as character
    local cDataQuery    as character
    local aFieldsMap    as object
    local nItemFrom     as numeric
    local nItemTo       as numeric
    local cOrderBy      as character
    local cOrdDirection as character
    local lDesc         as logical
    local aTemp         as array
    local cTempField    as character
    local nTemp         as numeric
    local nI            as numeric
    local nMax          as numeric
    local cCondition    as character
    local aQryValues    as array
    local oPrepStat     as object
    local aRetProps     as array
    local nRetProps     as numeric
    local xPropValue
    local nCount        as numeric
    local cCountQuery   as character
    local cProjQuery    as character
    local cSubQuery     as character
    lProcessed := .T.

    // Define o tipo de retorno do método
    self:SetContentType("application/json")

    // As propriedades da classe receberão os valores enviados por querystring
    // exemplo: /rest/microblog/v1/perfis?page=1&pageSize=5&order=-name&fields=name,email
    default self:page := 1
    default self:pageSize := 10
    default self:order := ""
    default self:fields := ""
    // Mapeia os campos da query com as propriedades
    aFieldsMap := {;
        {"email", "ZT0_EMAIL", "C", "ZT0_EMAIL"},;
        {"user_id", "ZT0_USRID", "C", "ZT0_USRID"},;
        {"name", "ZT0_NOME", "C", "ZT0_NOME"},;
        {"admin", "ZT0_ADMIN", "L", "ZT0_ADMIN"},;
        {"inserted_at", "ZT0_INS_AT", "D", "I_N_S_D_T_"},;
        {"updated_at", "ZT0_UPD_AT", "D", "S_T_A_M_P_"} ;
    }
    // montagem da paginação
    nItemFrom := (self:page - 1) * self:pageSize + 1
    nItemTo := (self:page) * self:pageSize

    // montagem da ordem
    if !Empty(Alltrim(self:order))
        aTemp := StrTokArr(self:order, ",")
        nMax := Len(aTemp)

        cOrderBy := ""
        for nI := 1 to nMax

            lDesc := (SubStr(aTemp[nI], 1, 1) == "-")
            if lDesc
                cTempField := SubStr(aTemp[nI], 2)
                cOrdDirection := " desc"
            else
                cTempField := aTemp[nI]
                cOrdDirection := " asc"
            endif

            nTemp := aScan(aFieldsMap, {|x| x[MAP_PROP] == cTempField})
            if nTemp > 0
                cOrderBy += aFieldsMap[nTemp, MAP_INTERNAL_FIELD] + cOrdDirection + ","
            endif
        next nI
        // Remove a última vírgula (,)
        cOrderBy := SubStr(cOrderBy, 1, Len(cOrderBy)-1)
    endif

    if Empty(cOrderBy)
        cOrderBy := "ZT0_NOME asc"
    endif

    // monta a condição para a query
    //  suporta filtros simples com operador LIKE -> campo like %valor%
    cCondition := "ZT0.D_E_L_E_T_ = ' ' "
    aQryValues := {}
    for nI := 1 To Len(self:aQueryString)
        cTempField := Lower(self:aQueryString[nI, QRY_PARAM_KEY])
        nTemp := aScan(aFieldsMap, {|x| x[MAP_PROP] == cTempField})
        // quando encontra cria a expressão e guarda o valor para atribuir
        if nTemp > 0
            if aFieldsMap[nTemp, MAP_TYPE] == "C"
                cCondition += "and ZT0." + aFieldsMap[nTemp, MAP_FIELD] + " like ? "
            else
                cCondition += "and ZT0." + aFieldsMap[nTemp, MAP_FIELD] + " = ? "
            endif
            // mantem par com tipo [QRY_VAL_TYPE] e valor [QRY_VAL_VALUE]
            aAdd(aQryValues, {aFieldsMap[nTemp, MAP_TYPE], self:aQueryString[nI, QRY_PARAM_VALUE]} )
        endif
    next nI

    // campos mapeados
    cProjQuery := "select ZT0_EMAIL, ZT0_USRID, ZT0_NOME, ZT0_ADMIN,"
    cProjQuery += "convert(varchar(23), I_N_S_D_T_, 21) ZT0_INS_AT,"
    cProjQuery += "convert(varchar(23), S_T_A_M_P_, 21) ZT0_UPD_AT,"
    cProjQuery += "ROW_NUMBER() OVER (order by "+ cOrderBy +") SEQITEM "

    // tabela e condições
    cSubQuery := "from " + RetSqlName("ZT0") + " ZT0 "
    cSubQuery += "where " + cCondition

    // query para os dados
    cDataQuery := "select * "
    cDataQuery += "from ( " + cProjQuery + cSubQuery + " ) QUERYDATA "
    cDataQuery += "where SEQITEM >= "+ cValToChar(nItemFrom) +" and SEQITEM <= "+ cValToChar(nItemTo) +" "

    oPrepStat := FwPreparedStatement():New(cDataQuery)
    for nI := 1 to Len(aQryValues)
        // atribui os valores de string com operador like
        if aQryValues[nI, QRY_VAL_TYPE] == "C"
            oPrepStat:SetLike(nI, aQryValues[nI, QRY_VAL_VALUE])

        // propriedade admin está com tipo lógico então faz igualdade 1=sim;2=não
        elseif aQryValues[nI, QRY_VAL_TYPE] == "L"
            if (Alltrim(aQryValues[nI, QRY_VAL_VALUE]) == "1" .Or. Alltrim(aQryValues[nI, QRY_VAL_VALUE]) == "true")
                cTemp := "1"
            else
                cTemp := "2"
            endif
            oPrepStat:SetString(nI, cTemp)
        endif
    next nI

    cDataQuery := oPrepStat:getFixQuery()

    cTempAlias := GetNextAlias()
    DbUseArea(.T., "TOPCONN", TcGenQry(,, cDataQuery), cTempAlias, .F., .F.)

    jResponse := JsonObject():New()
    jResponse["items"] := {}

    // monta as propriedades escolhidas para retorno
    aRetProps := StrTokArr(self:fields, ",")
    nRetProps := Len(aRetProps)

    if nRetProps == 0
        aRetProps := {"email", "user_id", "name", "admin", "inserted_at", "updated_at"}
        nRetProps := Len(aRetProps)
    endif

    while (cTempAlias)->(!EOF())
        aAdd(jResponse["items"], JsonObject():New())
        jTempItem := aTail(jResponse["items"])

        for nI := 1 to nRetProps
            // recupera o nome da propriedade
            cTemp := aRetProps[nI]

            // recupera o mapa propriedade x campo
            nTemp := aScan(aFieldsMap, {|x| x[MAP_PROP] == cTemp})

            if nTemp > 0
                // recupera o valor para a propriedade
                xPropValue := (cTempAlias)->(FieldGet(FieldPos(aFieldsMap[nTemp, MAP_FIELD])))

                // atribui o valor na propriedade
                if aFieldsMap[nTemp, MAP_TYPE] == "C"
                    jTempItem[cTemp] := RTrim(xPropValue)
                elseif aFieldsMap[nTemp, MAP_TYPE] == "L"
                    jTempItem[cTemp] := xPropValue == "1"
                else
                    jTempItem[cTemp] := xPropValue
                endif
            endif
        next nI
        (cTempAlias)->(DbSkip())
    enddo

    (cTempAlias)->(DbCloseArea())

    // Recupera a quantidade de registros
    cCountQuery := "select count(*) ROWS_QT " + cSubQuery

    oPrepStat := FwPreparedStatement():New(cCountQuery)
    for nI := 1 to Len(aQryValues)
        // atribui os valores de string com operador like
        if aQryValues[nI, QRY_VAL_TYPE] == "C"
            oPrepStat:SetLike(nI, aQryValues[nI, QRY_VAL_VALUE])

        // propriedade admin está com tipo lógico então faz igualdade 1=sim;2=não
        elseif aQryValues[nI, QRY_VAL_TYPE] == "L"
            if (Alltrim(aQryValues[nI, QRY_VAL_VALUE]) == "1" .Or. Alltrim(aQryValues[nI, QRY_VAL_VALUE]) == "true")
                cTemp := "1"
            else
                cTemp := "2"
            endif
            oPrepStat:SetString(nI, cTemp)
        endif
    next nI

    cCountQuery := oPrepStat:getFixQuery()
    DbUseArea(.T., "TOPCONN", TcGenQry(,, cCountQuery), cTempAlias, .F., .F.)

    if (cTempAlias)->(EOF())
        nCount := 0
    else
        nCount := (cTempAlias)->ROWS_QT
    endif
    jResponse["size"] := nCount

    self:SetResponse(jResponse:ToJson())
return lProcessed

Enter fullscreen mode Exit fullscreen mode

Top comments (0)