A finalidade do CrudSqlBuilder é transformar os metadados de um banco de dados em uma API
REST. Ele segue o conceito "Definir ao invés de programar": em vez de
escrever um backend do zero — ou contratar alguém para isso —, você descreve entidades, regras e
fluxos a partir das tabelas e campos do banco que já existe, e a engine entrega a API REST
publicada — com pool de conexão gerenciado, log de auditoria e agendamento de tarefas inclusos.
Conexões com o banco (versão Free). Em Server Control → ERP Database,
o campo Connection define quantas conexões simultâneas o servidor mantém com o banco de
dados. Na versão Free ele é um combo com 1, 2 e 3. O valor continua vindo do arquivo de
configuração do servidor e, na versão Free, qualquer valor diferente de 1, 2 ou 3 é ajustado para
3.
A solução é composta por dois aplicativos que operam juntos:
CrudSQLBuilder — onde você modela entidades, metadados e consultas SQL.
CrudSqlServer — o servidor que publica essa API e atende as requisições em produção.
Esta documentação segue a mesma ordem em que você normalmente monta uma API, do conceito mais
básico ao mais avançado:
Descriptor — descreve uma entidade e gera o CRUD automático (GET, POST, PUT, DELETE).
Sub-Table e Object Array — relacionam tabelas dentro da mesma resposta.
EndPoint — cria uma rota REST customizada além do CRUD automático.
PL/SQL — o motor de script declarativo, para regra de negócio que vai além de um CRUD simples.
Se você está começando agora, o caminho mais curto é: crie um Descriptor, teste o
CRUD automático gerado, e só avance para EndPoint e PL/SQL
quando precisar de uma regra que o CRUD padrão não cobre.
Recursos de integração e automação
Além do CRUD/EndPoint/PL/SQL, o CrudSqlServer também resolve necessidades comuns de integração e
rotina sem precisar de nenhuma ferramenta externa:
Scheduler.ini — agenda um endpoint pra rodar
sozinho, num intervalo fixo ou uma vez por dia, direto de um arquivo .ini — sem cron
externo, sem tarefa agendada do Windows.
SingleThreadScheduler.ini — o mesmo
agendamento, em fila única: os endpoints do arquivo rodam um de cada vez, para
integrações externas que só aceitam uma chamada por vez.
Node.js (integração) — chama um módulo Node.js
direto do script PL/SQL, pra regra de negócio que precisa de uma biblioteca ou processamento que o
SQL puro não cobre.
DLL (integração) — chama uma função de uma
DLL (Windows, 64 bits) direto do script PL/SQL — pra aproveitar bibliotecas nativas
prontas, como componentes fiscais (NF-e, NFS-e, CT-e), TEF, boleto ou equipamentos.
API externa — chama uma API externa
direto do script PL/SQL, sem Node.js e sem ferramenta à parte: o método, a autenticação e os
headers ficam na configuração do endpoint, e o próprio script monta a URL (com parâmetros e dados de
alias) e o body, e decide o que fazer em cada desfecho — Success,
Error e TimeOut. A resposta que o endpoint devolve ao seu cliente continua sendo a definida no
Result, independente da chamada externa.
AccessToken (Container de dados) — credencial de
integração pra hub/marketplace: o administrador cria o token e entrega pro cliente externo, sem
depender de um login cadastrado, podendo restringir a uma ou mais APIs específicas.
Tipos de arquivo (JSON & Streams) — o endpoint
trafega JSON (padrão) ou arquivos em binário puro (Stream): upload/download de
arquivo pesado (foto, PDF, XML), gravado em disco em vez de engordar o banco de dados.
Anonymous Access — endpoint que dispensa toda
autenticação, pra quando quem consome não tem como (ou não precisa) mandar credencial — um
marketplace importando uma imagem direto por URL, por exemplo.
Manual da API ( $help )
Toda API publicada pelo CrudSqlServer já vem com um portal de documentação interativo,
gerado automaticamente a partir do que está cadastrado (Descriptors, EndPoints, PL/SQL) — sem precisar
escrever nada à parte. Cobre autenticação, formato de data, tabela de endpoints por menu lateral, e um
Endpoint Tester pra testar cada chamada direto do navegador (ver
EndPoint).
Acesse acrescentando $help ao endereço base da API:
http://127.0.0.1:6081/CrudAPI/api/$help
http://<ip>:<porta>/<apiname>/api/$help
<apiname> é o nome da API cadastrada, e <porta> a porta configurada
em Server Control — o mesmo endereço base usado em qualquer outra chamada da API.
Datas (ISO 8601)
Todo valor de data ou data/hora enviado pra API — parâmetro de qualquer chamada, em qualquer
endpoint — deve estar no padrão ISO 8601:
Data:YYYY-MM-DD — ex.: 2026-08-11
Data e hora:YYYY-MM-DDTHH:MM:SS (fração de segundo opcional) — ex.:
2026-08-11T14:30:00
O valor é validado nesse formato antes do endpoint rodar — parâmetro fora do padrão é rejeitado com
HTTP 400, sem chegar a executar nada — e convertido
automaticamente para o literal aceito pelo banco de destino na hora de montar o comando; quem
escreve o script não precisa tratar isso.
O tipo declarado do parâmetro decide o que é aceito:
Parâmetro DATE: só a data. Um valor com hora é rejeitado com
HTTP 400, exceto quando a hora é
00:00:00 (2026-08-11T00:00:00) — nesse caso nada se perde e o valor é
tratado como a data.
Parâmetro DATE TIME: data e hora. Uma data sem hora é rejeitada
com HTTP 400.
A mesma regra vale para parâmetros na URL, no body e para os campos dentro de um array enviado no body.
O mesmo padrão vale para os valores devolvidos pela API: campos do tipo data saem
como YYYY-MM-DD (ex.: "2026-07-27") e campos de data e hora como
YYYY-MM-DDTHH:MM:SS (ex.: "2026-07-27T14:30:00", com a fração de segundo só
quando houver), sempre nesse formato fixo, independente das configurações regionais do servidor.
Quem decide é o tipo declarado do campo no Result, e não o tipo da coluna no banco: um
campo DATEnunca devolve hora, minuto e segundo, mesmo que a coluna
seja um timestamp; um campo DATE TIMEsempre devolve a hora (uma data
pura sai como 2026-07-27T00:00:00). Isso vale para o resultado do endpoint e para os
campos de um ObjectArray.
HTTPS e Certificado
Por padrão, o CrudSqlServer atende requisições em HTTP. Isso é suficiente pra uso
interno/local, mas se a API for acessada pela internet pública — inclusive por integrações de
terceiro, como um marketplace buscando uma imagem direto por URL (ver Tipos de
arquivo (JSON & Streams) e Anonymous Access em EndPoint) — HTTP puro
deixa de ser suficiente.
Credenciais em texto puro. O Basic Auth (usuário/senha) e o
header AccessToken não são criptografados em HTTP — são só codificados em base64,
trivial de decodificar. Qualquer ponto no caminho (roteador, provedor, Wi-Fi público) consegue
capturar e reutilizar a credencial livremente, sem precisar quebrar senha nenhuma.
Dado de negócio exposto. Tudo que passa pelo endpoint — pedidos, preços, dados de
cliente, e no caso do Stream, o próprio arquivo (foto, PDF, XML de nota fiscal/contrato) — trafega
legível. Qualquer um no meio do caminho lê o conteúdo.
Sem proteção contra adulteração. HTTP não garante integridade — sem verificação,
é possível alterar a requisição ou a resposta em trânsito (trocar uma foto, mudar um preço num JSON)
sem que ninguém perceba.
Risco de LGPD. Se a API trafega dado pessoal, transmitir isso em texto puro pela
internet pode ser considerado falta de medida técnica de segurança adequada, exigida pela lei
(Art. 46) — risco jurídico, não só técnico.
Endpoints Anonymous Access merecem atenção à parte: ali não há credencial pra
vazar, mas o conteúdo em si (a imagem, o PDF) ainda trafega sem criptografia — aceitável pra um dado
já pensado como público, mas vale ser uma decisão consciente, não o padrão por acaso.
Risco prático/comercial. Plataformas como marketplaces vêm ficando mais
restritivas com HTTP puro — podem recusar ou penalizar a importação de um arquivo vindo de fonte
não-HTTPS.
Uso só interno/local, HTTP é tolerável. Qualquer coisa acessada de fora (o caso do Anonymous Access
buscado por terceiro, por exemplo), HTTPS deixa de ser opcional.
Ativando HTTPS
Em Server Control → API Web Server, ative o toggle Enable HTTPS e
informe o Certificate File e o Key File (arquivos .pem
do certificado e da chave privada) — Key Password só é necessário se a chave privada
estiver protegida por senha. Clique em Save e reative o servidor.
Sobre o certificado
O certificado precisa ser emitido por uma autoridade certificadora (CA) reconhecida —
Let's Encrypt é gratuita e cobre a maioria dos casos.
⚠️
Dois pontos que valem atenção antes de ativar:
HTTP e HTTPS não convivem na mesma porta — assim que o certificado é ativado,
uma chamada em http:// deixa de funcionar naquela porta. É intencional: evita que o
servidor continue aceitando conexão sem criptografia por engano.
Certificado autoassinado (gerado localmente, sem CA por trás) serve pra
validar a configuração, mas exige um domínio de verdade apontando pro servidor —
não existe certificado confiável pra um endereço IP puro — e navegadores/clientes HTTP rejeitam a
conexão por padrão, com um aviso do tipo "conexão não é privada" /
NET::ERR_CERT_AUTHORITY_INVALID.
HTTPS protege o transporte — quem mais tem acesso ao endpoint continua sendo definido
por quem pode chamar ele. Combinado com Authorized APIs no
AccessToken (restringe a quais APIs aquele token dá acesso) e com
Anonymous Access (ver EndPoint, só onde é proposital), essas
três peças juntas formam a política de segurança completa de uma integração exposta publicamente.
Autenticação
O CrudSqlServer exige autenticação nas chamadas aos endpoints. O modelo padrão do sistema é o
Basic Authentication — usuário e senha. Existem ainda dois
mecanismos alternativos, para os casos em que usuário e senha não se encaixam:
o AccessToken e o
Anonymous Access.
Modelo
Como o consumidor se identifica
Quando usar
Basic Authentication (padrão)
Usuário e senha
Todo consumo da API por sistemas e pessoas com login cadastrado. É o que vale para qualquer endpoint, a menos que ele use um dos dois modelos abaixo.
AccessToken
Token no header AccessToken ou na URL (?AccessToken=...)
Integrações do tipo hub/marketplace e webhooks, sem usuário e senha, mas ainda exigindo uma credencial. Também carrega um Container de dados.
Anonymous Access
Nenhuma
Endpoint em que quem consome não tem como se autenticar — por exemplo, um marketplace buscando uma imagem direto por URL. É ligado endpoint por endpoint.
Basic Authentication (padrão)
É o modelo padrão do sistema: toda chamada informa um usuário e uma
senha. As credenciais são cadastradas pelo administrador do
CrudSqlServer — quem vai consumir a API deve solicitá-las ao administrador.
O usuário e a senha seguem no header HTTP padrão Authorization, no formato Basic
(usuário e senha separados por dois pontos e codificados em base64). Os clientes HTTP comuns já
montam esse header sozinhos: no Postman, em Authorization → Basic
Auth; no Python (requests), pelo parâmetro auth=(usuario, senha);
no curl, pela opção -u:
Sem credencial, ou com usuário/senha inválidos, a chamada é rejeitada com
HTTP 401.
Atenção: o Basic Authentication codifica as credenciais em base64,
mas não as criptografa. Em HTTP puro elas podem ser capturadas no caminho. Para uma
API acessível pela internet, use HTTPS — ver
HTTPS e Certificado.
Alternativas ao usuário e senha
AccessToken — credencial de integração criada pelo
administrador e entregue ao cliente externo, sem depender de um login cadastrado, podendo ser
restrita a uma ou mais APIs. Pode ser enviada em um header ou na própria URL.
Anonymous Access — opção do endpoint que dispensa toda
autenticação, para quando o consumidor não tem como (ou não precisa) mandar nenhuma credencial.
Resumindo os três níveis, do mais aberto ao mais restrito: Anonymous Access (nenhuma
autenticação) → AccessToken (sem usuário e senha, por header ou URL) →
Basic Authentication (usuário e senha, o padrão do sistema).
Scheduler.ini
Agendador de tarefas do servidor — roda um endpoint periodicamente, sozinho, sem
nenhuma chamada externa disparando ele. É o complemento natural do modelo sob demanda (alguém chama
o endpoint): aqui é o próprio servidor que chama, num intervalo fixo ou uma vez por dia, direto do
arquivo scheduler*.ini — sem precisar de nenhum cliente HTTP, cron externo ou tarefa
agendada do Windows.
Arquivo e seções
Um ou mais arquivos scheduler*.ini (qualquer nome começando com scheduler
e terminando em .ini), na mesma pasta do executável do servidor. Cada
seção do arquivo é uma tarefa independente — o nome da seção é livre, só serve pra
identificar a tarefa no log:
O servidor varre a pasta a cada 15 segundos procurando por tarefas vencidas, em
todos os arquivos scheduler*.ini encontrados — liga e desliga junto com o próprio
servidor (sem o servidor ativo, nenhuma tarefa roda). Cada arquivo processa suas
seções em sequência, uma de cada vez; arquivos diferentes rodam em paralelo entre si. Se uma tarefa
ainda estiver rodando quando os 15s seguintes chegarem, o arquivo dela é pulado nessa varredura —
nunca roda duas instâncias da mesma tarefa ao mesmo tempo.
endpoint= — endereço completo, como uma chamada real
O valor é o mesmo caminho que apareceria numa chamada HTTP de verdade — API, /api/
literal e o caminho do endpoint, com ? e parâmetros de query se precisar:
A tarefa executa o endpoint internamente (mesmo caminho de código de uma requisição
GET de verdade, sem round-trip HTTP de verdade) — aparece no log com usuário SYSTEM e o
nome do arquivo/seção no lugar do IP, pra deixar claro que veio do agendador, não de um cliente
externo.
ExecutionMode: Interval
Roda repetidamente, a cada IntervalMinutes minutos (aceita fração — 0.5 =
30 segundos — sempre com ponto como separador decimal). Existe um piso obrigatório
de 10 segundos: qualquer valor menor (ou ausente/inválido) é tratado como 10s.
ExecutionMode: OnceDaily
Roda uma vez por dia, no horário declarado em DailyTime (formato
HH:MM, 24h). Se o servidor estiver desligado no horário programado, a tarefa dispara
assim que ele voltar (não perde a execução do dia, só atrasa) — mas nunca roda duas vezes no mesmo
dia.
Active — ligar ou desligar a tarefa
Cada seção aceita a chave Active=True ou Active=False. Sem a chave, a
tarefa é considerada ativa. Com Active=False a tarefa continua no
arquivo (por exemplo, como modelo pronto para uso), mas não é executada: não chama
o endpoint, não grava lastrun e não gera log. A mudança vale a partir da próxima
varredura, sem reiniciar o servidor. Vale para o scheduler*.ini e para o
SingleThreadScheduler.ini.
Depois de cada execução, o próprio servidor grava lastrun= na seção do arquivo
.ini, com a data/hora da última vez que rodou — é assim que ele sabe se uma tarefa já
está vencida ou não. Não precisa (nem deve) editar esse campo manualmente.
Notificação por e-mail (opcional)
Adicionar email= na seção manda o resultado da execução por e-mail — só quando o
endpoint devolve algum registro de verdade (resultado vazio nunca gera e-mail, só
fica no log). O envio é assíncrono e durável: a tarefa só grava um arquivo de fila
em disco na hora (sobrevive a reiniciar ou até derrubar o servidor) — uma thread dedicada, separada,
consome essa fila e manda de verdade depois. Arquivo enviado com sucesso é movido pra uma subpasta
Sent, onde fica guardado por 30 dias antes de ser apagado automaticamente.
O resultado (JSON) vai como anexo do e-mail, não no corpo. Assunto/título/mensagem/
rodapé são templates configuráveis na tela Email Admin do CrudSqlServer (mesmo lugar
do SMTP), com estes tokens disponíveis:
{{JOB_NAME}} -- nome da seção (ex: EmitirNotasPendentes)
{{FILE_NAME}} -- nome do arquivo scheduler*.ini
{{ENDPOINT}} -- o endereço declarado em endpoint=
{{DATE_TIME}} -- data/hora da execução
{{STATUS}} -- status HTTP devolvido pelo endpoint
{{RECORD_COUNT}} -- quantidade de registros no resultado
{{RESULT}} -- o JSON do resultado (cru)
Notificação por FTP (opcional)
Mesmo princípio do e-mail, mas subindo o resultado (JSON) como arquivo pra um servidor FTP —
declarado com ftphost=, ftpuser= e ftppassword=
(ftpport= opcional, padrão 21; ftpdir= opcional, pasta
remota):
Mesma fila durável em disco, mesma thread dedicada, mesma regra de só disparar quando o resultado
tem registro. O nome do arquivo remoto é <seção>_aammdd_hhmm.json — vale notar
que ele não inclui os segundos: se a mesma tarefa rodar mais de uma vez dentro do
mesmo minuto (só possível com IntervalMinutes bem curto), o segundo envio sobrescreve o
primeiro no servidor FTP.
Para integrações que só aceitam uma chamada por vez, existe o modelo de fila única:
SingleThreadScheduler.ini.
SingleThreadScheduler.ini
Agendador de tarefas em fila única — o mesmo modelo do
Scheduler.ini (o servidor chama o endpoint sozinho, num intervalo fixo ou
uma vez por dia), com uma diferença: todos os endpoints do arquivo são organizados em fila
única para o processamento, ordenada pelo lastrun — ou seja, não há
processamento simultâneo dos endpoints contidos no arquivo.
Algumas integrações externas (um ERP, uma plataforma de e-commerce) só aceitam uma chamada por
vez: a próxima só pode começar quando a anterior termina. Para esse caso existe o arquivo
SingleThreadScheduler*.ini (qualquer nome começando com SingleThreadScheduler,
na pasta do executável), com as mesmas seções e chaves do scheduler*.ini —
endpoint, Active, ExecutionMode, IntervalMinutes,
DailyTime, e-mail e FTP. Só muda o jeito de executar:
Cada arquivo tem uma thread própria, que roda enquanto o servidor estiver ativo
e para junto com ele.
Todos os endpoints do arquivo formam uma fila única: roda um de cada
vez, e a próxima chamada só começa quando a anterior termina — mesmo que sejam endpoints
diferentes.
Entre as tarefas vencidas, vai primeiro a que rodou há mais tempo (quem nunca
rodou vai na frente; em caso de empate vale a ordem das seções no arquivo). A cada tarefa que
termina, a fila é reavaliada, então nenhuma fica para trás.
O intervalo conta a partir do fim da execução anterior, e vale o mesmo piso de
10 segundos do scheduler*.ini.
Arquivos diferentes rodam em paralelo entre si.
Opcionalmente, um intervalo entre um endpoint e o próximo (IntervalEndpointsMS,
abaixo), para respeitar o limite de requisições da plataforma.
# SingleThreadScheduler_Shoppub_id_cli01.ini — uma conta da plataforma, uma fila
[SincronizarPedidos]
endpoint=/crudapi/api/shoppub/sincronizarPedidos
ExecutionMode=Interval
IntervalMinutes=5
[SincronizarEstoque]
endpoint=/crudapi/api/shoppub/sincronizarEstoque
ExecutionMode=Interval
IntervalMinutes=10
Intervalo entre os endpoints — IntervalEndpointsMS
Muitas plataformas aceitam uma chamada por vez e limitam quantas chamadas por período.
Quando todos os endpoints do arquivo falam com a mesma plataforma, a fila já garante que um só roda por
vez; a chave IntervalEndpointsMS garante também um intervalo entre um endpoint e o seguinte.
Ela fica numa seção própria, chamada [Queue], separada das seções das tarefas (que são as que
têm a chave endpoint):
IntervalEndpointsMS é um número inteiro em milissegundos (2000 = 2 segundos).
O intervalo vale depois de cada endpoint da fila, antes de qualquer outro endpoint do mesmo
arquivo — inclusive quando o endpoint terminou com erro, porque a plataforma contou a chamada do mesmo jeito.
Ela só existe entre dois endpoints seguidos: com a fila vazia, o servidor continua verificando a cada
poucos segundos, como sempre.
Sem a seção [Queue], sem a chave, ou com 0, não há intervalo.
O arquivo é lido de novo a cada passada da fila: alterar o IntervalEndpointsMS vale na hora, sem
reiniciar o servidor.
Cada arquivo tem o seu próprio IntervalEndpointsMS — duas plataformas com limites diferentes ficam em
arquivos diferentes.
Para espaçar chamadas feitas dentro de um mesmo endpoint (por exemplo, uma por linha de um
#For), use o comando #IntervalMS ( ) no script.
Um arquivo por integração. Como a fila é única, uma tarefa que demora atrasa todas as
outras do mesmo arquivo. Por isso, coloque no mesmo arquivo só tarefas que falam com a
mesma plataforma e a mesma conta, e use um arquivo separado para cada integração
(por exemplo SingleThreadScheduler_Shoppub_id_cli01.ini e
SingleThreadScheduler_Shoppub_id_cli02.ini) — assim uma não espera pela outra.
Ao parar o servidor, a fila termina a tarefa que já está em execução e não inicia nenhuma outra.
Node.js (integração)
O CrudSqlServer pode invocar funções escritas em Node.js, rodando localmente no
mesmo servidor da API, através do comando #CallNodeJS ( ) do motor de
script — útil pra cobrir o que SQL/PL-SQL não resolve bem, como chamar uma API externa (Correios,
SEFAZ, gateway de pagamento) ou aplicar uma transformação mais livre.
Detalhes completos do comando, sintaxe e os três blocos (TimeOut/Error/
Success) em #CallNodeJS ( ). Esta seção fala só de
instalação, estrutura dos módulos e como testar — não repete os exemplos de script, que já estão
lá.
A mesma função de módulo pode ser acionada de dois jeitos — processada sob demanda, por uma
chamada externa que dispara o endpoint (#CallNodeJS), ou agendada pra rodar sozinha,
sem ninguém precisar chamar, através do agendador de tarefas do servidor
(scheduler*.ini). São dois jeitos complementares de chegar na mesma função, não dois
recursos separados.
Instalação
O Node.js não vem embutido no instalador do CrudSqlServer — precisa ser instalado
separadamente, na máquina do servidor, pelo próprio usuário. Baixe a versão LTS em
nodejs.org e rode o instalador padrão do Windows (.msi) — nenhuma
configuração especial é necessária no instalador do Node em si, só ter o node.exe
disponível na máquina depois.
Configuração na aplicação
Na tela API Web Server do CrudSqlServer existe o campo Node.js Path,
com um botão de busca (igual ao usado pra apontar o banco de dados) — aponta pro node.exe
instalado. Se o Node.js estiver no local padrão da instalação
(C:\Program Files\nodejs\node.exe), o campo já sugere esse caminho sozinho. Esse é o
único passo necessário pra ligar a integração — enquanto o campo estiver vazio, ela fica desligada.
Módulos
Cada arquivo de função exporta exatamente uma função (assíncrona) — o nome do
arquivo, sem .js, é o que vai em Module: "nome" no script. A função
recebe os parâmetros de Params já traduzidos pra um objeto JavaScript comum, e o que
ela devolver (return) fica disponível de volta no script, através do alias declarado
(:alias.campo).
Os arquivos ficam dentro da pasta NodeBridge\Modules, na mesma pasta que está instalado
o CrudSqlServer.exe — exemplo, se o CrudSqlServer estiver instalado em
C:\CrudSqlServer\:
C:\CrudSqlServer\NodeBridge\Modules\
O sistema varre essa pasta (e todas as subpastas dela) em busca do arquivo — então dá pra
organizar as funções em subpastas por assunto, do jeito que fizer mais sentido:
⚠️Nunca com barra no início —
Module: "Fretes/calcularFrete" está correto;
Module: "/Fretes/calcularFrete" (começando com barra) não funciona:
o sistema entende como caminho absoluto a partir da raiz do disco, não relativo à pasta
Modules, e a chamada falha com module not found.
Contrato obrigatório do módulo
JSON é obrigatório só na própria chamada Node. O params que a função
recebe e o return que ela devolve nessa mesma invocação — a troca
direta entre o CrudSqlServer e o módulo, por trás de #CallNodeJS — precisam ser sempre
serializáveis em JSON: objeto, array, string, número, boolean ou null. Nunca uma
função, um Buffer, uma Promise não resolvida ou qualquer outro tipo que o JSON não representa.
Essa regra é só sobre essa troca específica — não vale pro callback tardio que o
módulo eventualmente fizer por conta própria depois (ver token logo abaixo): essa é uma chamada
HTTP comum, que deve seguir os parâmetros esperados pelo endpoint que está sendo chamado, seja
parâmetro por URL ou corpo JSON — a escolha não é livre, é o próprio endpoint de destino que dita
o formato.
Callback efêmero. Toda função de módulo recebe um segundo argumento
(contexto) com um usuário e senha efêmeros (válidos por 7 minutos,
um par novo gerado a cada chamada) e o endereço do próprio servidor — pra cobrir qualquer situação
em que o módulo precise reportar um resultado depois de já ter devolvido controle
pro CrudSqlServer (ex: um #CallNodeJS que estourou o TimeOut, mas o
processamento continuou rodando por conta do módulo).
module.exports = async function (params, contexto) {
// contexto.username / contexto.password -> autenticam (Basic Auth)
// uma chamada de volta pro CrudSqlServer, quando/se precisar
// reportar algo depois.
// contexto.baseUrl -> endereço do próprio servidor (ex:
// "http://127.0.0.1:6081"), já com a porta real configurada em
// Server Control - nunca fixe a porta no arquivo .js.
...
};
Como usar pra chamar de volta: não existe um "endpoint de callback" fixo nem um
callbackUrl pronto — quem decide pra qual endpoint chamar, com quais dados (parâmetro
na URL ou corpo JSON, GET/POST/PUT, como preferir) é o próprio módulo. contexto.username/
contexto.password substituem a autenticação normal, via Basic Auth:
Buffer.from(`${contexto.username}:${contexto.password}`).toString("base64")
Autenticado assim, o acesso é irrestrito (não passa pelas mesmas regras de
permissão por API de um usuário comum) — é uma decisão deliberada: é o próprio cliente quem
programa o módulo e decide o que ele chama, então o controle de acesso ali é responsabilidade dele.
No log do endpoint, essa chamada aparece com o usuário NodeJS, pra identificar que
veio de um módulo, não de um usuário cadastrado (o usuário/senha efêmeros reais nunca ficam
gravados no log).
Concorrência
O CrudSqlServer mantém um pool de processosnode.exe residentes
(não um processo novo por chamada) — mesma estratégia já usada pro pool de conexão com o banco.
Um worker que trava, morre ou estoura o tempo limite é descartado e substituído automaticamente,
sem afetar o restante do pool.
Como testar
O instalador já vem com um arquivo de exemplo pronto, consultar_cep.js — consulta o
ViaCEP (gratuito, sem
cadastro/chave), pra você testar a integração de ponta a ponta sem precisar configurar nenhuma
credencial primeiro. Ele já fica em NodeBridge\Modules\consultar_cep.js, no mesmo
diretório do servidor — não precisa copiar nada, só criar o endpoint abaixo.
1. Crie o Endpoint
Método: GET
Show in $help: True (ou False, se preferir só
testar antes de publicar)
Processing Mode: PL/SQL Script
2. Declare o Parâmetro
Um único parâmetro, obrigatório:
Nome: CEP
Tipo: STRING
Required: sim
3. Escreva o Script (PL/SQL Script)
Copie exatamente este bloco — Module aponta pro arquivo de exemplo, sem o
.js; o alias :RESP é quem recebe o retorno do módulo dentro de
Success:
As duas linhas #SetResponseCode(504) são opcionais — sem elas, o
endpoint responderia sempre HTTP 200 (mesmo em timeout/erro),
com o desfecho visível só pelo campo status do corpo. Aqui optamos por sinalizar
timeout e erro também no status HTTP (504 Gateway Timeout, já que
é uma falha de uma API externa) — ver #SetResponseCode ( )
para o comando completo, incluindo o aviso sobre alerta por e-mail em códigos ≥ 500.
4. Declare o Result
Cinco campos, todos STRING — precisam bater com os nomes usados nos
#SetVarResult acima:
status
logradouro
bairro
cidade
uf
5. Chame o endpoint
Com o servidor ativo, uma chamada GET simples, passando o CEP por querystring:
GET /CrudApi/api/<sua_api>/<seu_endpoint>?cep=01001000
💡Se der erro 500 "module not found", confira se o campo
Node.js Path (tela API Web Server) está apontando pro
node.exe instalado — enquanto esse campo estiver vazio, a integração fica desligada e
nenhum #CallNodeJS funciona, em nenhum endpoint.
DLL (integração)
O CrudSqlServer pode invocar funções de uma DLL (biblioteca nativa do Windows),
instalada no mesmo servidor da API, através do comando #CallDLL ( ) do
motor de script — útil pra aproveitar bibliotecas prontas, escritas em Delphi, C/C++ ou qualquer
linguagem que gere DLL: componentes fiscais (NF-e, NFS-e, CT-e, MDF-e), TEF, boleto, PIX, balança,
impressora ou o SDK de um parceiro.
Detalhes completos do comando, sintaxe e os três blocos (TimeOut/Error/
Success) em #CallDLL ( ). Esta seção fala só de
configuração, estrutura das bibliotecas, contrato que a DLL precisa seguir e como testar. O modelo é
o mesmo do Node.js (integração): muda só o que roda do outro lado.
Configuração na aplicação
Não há nada para configurar nem para instalar à parte: o executável que carrega as DLLs
(CrudDLLHost.exe) já vem com o instalador do CrudSqlServer, dentro da pasta
DLLBridge. Ele só entra em execução na primeira chamada de um
#CallDLL — enquanto nenhum endpoint usa o comando, nada fica rodando.
Bibliotecas
As DLLs ficam dentro da pasta DLLBridge\Libraries, na mesma pasta em que está
instalado o CrudSqlServer.exe — exemplo, se o CrudSqlServer estiver instalado em
C:\CrudSqlServer\:
C:\CrudSqlServer\DLLBridge\Libraries\
Só as DLLs que estão nessa pasta (ou nas subpastas dela) podem ser chamadas — é ela que define o que
está liberado no servidor. Dá pra organizar por assunto, do jeito que fizer mais sentido:
No script, Library: é "arquivo + função" num campo só, igual ao Module
do #CallNodeJS: o caminho a partir de Libraries\, sem o
.dll, seguido de um ponto e do nome da função:
O nome da função é o que vem depois do último ponto (depois da última barra); o
que vem antes é a biblioteca. Sem a parte da função (ex: Library: "crud_documentos")
é erro de configuração do endpoint.
⚠️Sempre relativo à pasta Libraries — nunca com barra no início,
letra de unidade (C:\) ou ... Qualquer caminho que tente sair da pasta
Libraries é recusado e a chamada falha com library not found: o script nunca
escolhe um arquivo arbitrário do disco.
Arquivos auxiliares que a DLL precisar (outras DLLs das quais ela depende, arquivos
.ini de configuração, schemas, certificados) ficam na mesma pasta da
DLL.
Os arquivos da pasta Libraries nunca ficam presos. O servidor não
carrega a DLL direto dali: na primeira chamada, ele copia todas as .dll da pasta da
biblioteca (a DLL e as DLLs das quais ela depende) para uma pasta temporária própria e carrega a
cópia. Por isso, para trocar uma DLL por uma versão nova, basta substituir o
arquivo em DLLBridge\Libraries, a qualquer momento, com o servidor rodando: a próxima
chamada percebe a mudança e já usa a versão nova, sem reiniciar nada. Se a cópia ainda não puder
ser feita (o arquivo ainda está sendo gravado), a versão atual continua atendendo e a troca é
tentada de novo na chamada seguinte.
Só as .dll são copiadas — o .ini, os schemas e as pastas de saída
continuam na pasta original. Por isso a DLL nunca deve procurar esses arquivos pelo
caminho dela mesma (que é a cópia temporária): ela recebe a pasta original em
libraryFolder, no contexto de cada chamada (ver logo abaixo), e lê e grava tudo a
partir dali.
⚠️DLL de 64 bits — o CrudSqlServer é 64 bits, então a DLL (e as dependências
dela) também precisam ser 64 bits. Uma DLL de 32 bits não carrega e a chamada
falha com erro de configuração.
Contrato obrigatório da DLL
Toda DLL chamada por #CallDLL exporta duas funções obrigatórias, com
convenção stdcall. Toda a troca é feita em texto UTF-8 terminado em
zero (PAnsiChar no Delphi, char* no C) — nunca um objeto, uma
string nativa da linguagem ou um ponteiro para estrutura:
function CrudDLL_Call(AFunction, AParamsJson, AContextJson: PAnsiChar;
out AResultJson: PAnsiChar): Integer; stdcall;
procedure CrudDLL_Free(AText: PAnsiChar); stdcall;
AFunction — o nome da função escrito depois do ponto em Library: no script. Uma mesma DLL
atende quantas funções quiser: é ela quem decide o que fazer com cada nome.
AParamsJson — os parâmetros de Params:, já montados como um
objeto JSON (ex: {"uf":"SP","ambiente":"2"}).
AContextJson — o contexto da chamada, com o usuário e a senha efêmeros, o
endereço do servidor (ver Callback efêmero logo abaixo) e
libraryFolder: a pasta original da biblioteca em DLLBridge\Libraries,
de onde a DLL lê o .ini, os schemas e onde grava a saída.
AResultJson — o texto que a DLL devolve, alocado por ela mesma: um JSON
(objeto ou array) com o resultado quando deu certo, ou um objeto com message
quando deu errado (ex: {"message":"Certificado vencido"}).
Retorno — 0 = sucesso (vai para o bloco Success);
qualquer outro número = erro (vai para o bloco Error).
CrudDLL_Free — o servidor chama sempre, logo depois de ler
AResultJson, para a própria DLL liberar a memória que ela alocou. Quem aloca é a
DLL, quem libera é a DLL.
Opcionalmente, a DLL pode exportar também function CrudDLL_Info: PAnsiChar; stdcall;,
devolvendo um JSON com a versão e a lista de funções disponíveis (ex:
{"version":"1.0.0","functions":["ValidarCPF","ValidarCNPJ"]}) — quando existe, o
servidor registra essas informações no log ao carregar a biblioteca.
Exemplo mínimo em Delphi — uma DLL com uma função ValidarCPF:
library crud_documentos;
uses
System.SysUtils, System.JSON;
// Aloca o texto de resposta - liberado depois em CrudDLL_Free.
function NovoTexto(const AValor: string): PAnsiChar;
var
LBytes: TBytes;
begin
LBytes := TEncoding.UTF8.GetBytes(AValor);
GetMem(Result, Length(LBytes) + 1);
if Length(LBytes) > 0 then
Move(LBytes[0], Result^, Length(LBytes));
Result[Length(LBytes)] := #0;
end;
function ErroJson(const AMensagem: string): PAnsiChar;
var
LObj: TJSONObject;
begin
LObj := TJSONObject.Create;
try
LObj.AddPair('message', AMensagem);
Result := NovoTexto(LObj.ToJSON);
finally
LObj.Free;
end;
end;
function CrudDLL_Call(AFunction, AParamsJson, AContextJson: PAnsiChar;
out AResultJson: PAnsiChar): Integer; stdcall;
var
LParams: TJSONValue;
LCpf: string;
begin
Result := 1;
AResultJson := nil;
try
LParams := TJSONObject.ParseJSONValue(UTF8ToString(AParamsJson));
try
if SameText(UTF8ToString(AFunction), 'ValidarCPF') then
begin
LCpf := LParams.GetValue<string>('cpf', '');
if CpfValido(LCpf) then // sua regra de validação
AResultJson := NovoTexto('{"valido":true}')
else
AResultJson := NovoTexto('{"valido":false}');
Result := 0;
end
else
AResultJson := ErroJson('Function not found: ' + UTF8ToString(AFunction));
finally
LParams.Free;
end;
except
on E: Exception do
begin
Result := 1;
AResultJson := ErroJson(E.Message);
end;
end;
end;
procedure CrudDLL_Free(AText: PAnsiChar); stdcall;
begin
FreeMem(AText);
end;
exports
CrudDLL_Call,
CrudDLL_Free;
begin
end.
⚠️Nenhuma exceção pode sair da DLL — todo o corpo de
CrudDLL_Call fica dentro de um try/except que converte o erro em
{"message": ...} com retorno diferente de zero, como no exemplo. Uma exceção que
escapa (ou um access violation) derruba o processo que carregou a DLL: o servidor continua
no ar, mas aquela chamada termina no bloco Error e o processo é substituído.
Callback efêmero. Assim como no Node.js, AContextJson traz um
usuário e senha efêmeros (válidos por 7 minutos, um par novo a cada chamada) e o
endereço do próprio servidor — para a DLL reportar um resultado depois de já ter
devolvido o controle (ex: um #CallDLL que estourou o TimeOut, mas o
processamento continuou na DLL). A chamada de volta é uma requisição HTTP comum, autenticada por
Basic Auth com esse usuário e senha, para o endpoint que a própria DLL escolher; no
log ela aparece com o usuário DLL.
As DLLs não rodam dentro do CrudSqlServer. O servidor mantém um pool de
processosCrudDLLHost.exe residentes — a mesma estratégia do pool de
node.exe e do pool de conexão com o banco — e cada processo carrega as DLLs (a cópia
temporária) uma vez e as mantém carregadas entre as chamadas, até a DLL ser trocada. Os
processos só sobem na primeira chamada de um #CallDLL. Isso garante três coisas:
O TimeOut é real — quando o tempo limite estoura, o processo é encerrado e
substituído; a DLL não fica presa segurando o servidor.
Uma DLL com defeito não derruba a API — um travamento ou access
violation atinge só aquele processo, que é descartado e substituído automaticamente, sem
afetar o restante do pool.
A DLL não precisa ser thread-safe — cada processo atende uma chamada de cada
vez. Por outro lado, chamadas seguidas podem cair em processos diferentes: a DLL não deve contar
com um estado guardado em memória de uma chamada para a outra (grave no banco, ou devolva no
resultado, o que precisar ser lembrado).
Usando com o ACBr
Para emissão fiscal, o caminho é uma DLL adaptadora fina — por exemplo
crud_acbr_nfe.dll — escrita em Delphi seguindo o contrato acima, que por dentro usa os
componentes do ACBr (ou a ACBrLib) e devolve o resultado em JSON.
A adaptadora, a DLL do ACBr e o .ini de configuração dele (certificado, ambiente,
pasta de schemas) ficam juntos, na mesma subpasta — e a adaptadora monta o caminho do
.ini e dos schemas a partir de libraryFolder do contexto, nunca do caminho
da própria DLL:
Como cada processo do pool atende uma chamada de cada vez, a versão single-thread
da ACBrLib é suficiente — e precisa ser a de 64 bits.
Como testar
O instalador já vem com uma DLL de exemplo pronta, crud_documentos.dll (com o
código-fonte em DLLBridge\Exemplos), que valida CPF — sem dependência externa nem
credencial, pra você testar a integração de ponta a ponta. Ela já fica em
DLLBridge\Libraries\crud_documentos.dll, no mesmo diretório do servidor — só criar o
endpoint abaixo.
💡Se der erro 500 "library not found", confira se a DLL está dentro de
DLLBridge\Libraries com o nome usado em Library: (a parte antes do ponto).
API externa
Integra o endpoint com uma API externa — um ERP, uma plataforma de e-commerce, um
serviço de consulta — direto do script PL/SQL, sem Node.js e sem ferramenta à
parte. É o jeito mais curto de buscar ou enviar um dado em outro sistema por HTTP.
A divisão é simples: o que é fixo na chamada — o método (GET,
POST…), a autenticação, os headers e a URL base — fica na configuração do endpoint, com
senhas e tokens guardados como variáveis protegidas; o que muda a cada execução — o
caminho e a query da URL, com parâmetros e dados de alias, e o body — é escrito no próprio script. O script
decide também o que fazer em cada desfecho: Success, Error e
TimeOut.
A chamada externa é independente do endpoint: o que o seu cliente recebe continua
sendo o que o tipo do endpoint e o Result definem. Para lógica que precisa de uma biblioteca ou de
processamento mais livre, continua existindo o Node.js (integração).
Referência completa — configuração do request, montagem da URL, body e desfechos — em
#CallHTTP ( ), na aba CrudSQLServer.
AccessToken (Container de dados)
Pensado pra integrações do tipo hub/marketplace: o problema real que resolve é evitar
que o cliente externo precise mandar, em toda chamada, um dado de contexto que já deveria estar fixo
pra aquela integração — por exemplo, CHAVE_EMPRESA, identificando de qual empresa/loja
são os dados daquele cliente específico. Em vez de exigir esse parâmetro em todo endpoint chamado,
ele fica gravado uma vez no Container do token — uma lista livre de parâmetros
(nome/valor), definida por quem criou o token — e o script lê de lá quando
precisar, através de #ParamTokenValue ( ) (ver abaixo). A chamada do hub fica mais
enxuta, sem precisar saber ou repetir um dado que não é da conta dele.
Complementarmente, o AccessToken também funciona como credencial de autenticação
alternativa: o administrador cria o token na tela Token Parameters
do CrudSqlServer e entrega o valor gerado pro cliente externo — sem estar vinculado a nenhum
usuário/login cadastrado, dispensando usuário/senha na chamada.
Como enviar
De duas formas, à escolha de quem está integrando — o servidor aceita qualquer uma das duas:
Header HTTP, fixo, sem esquema/prefixo:
AccessToken: 3fa85f64-5717-4562-b3fc-2c963f66afa6
Query string da URL, para quando quem está chamando não tem controle sobre os
headers da requisição — caso típico de webhook: plataformas como Mercado Livre ou
Shopify só permitem cadastrar uma URL fixa de callback, sem opção de anexar um header customizado
na notificação que elas enviam. Nesse caso o token fica embutido na própria URL registrada na
plataforma:
Se as duas formas forem enviadas na mesma requisição, o header tem prioridade.
Fora isso funcionam de forma idêntica — mesma validação, mesmo Container, mesmo Authorized APIs.
O token em si é opcional — mas se o script do endpoint usa
#ParamTokenValue ( ) (ver abaixo) em algum lugar, a chamada passa a
exigir um token válido (por header ou por URL) com aquele campo no Container, só
por causa disso. Se o script não usa a macro em lugar nenhum, mandar o token (ou não) não faz
nenhuma diferença — nada é bloqueado.
⚠️O Container é só para uso interno do script — nunca substitui o parâmetro do
endpoint. O AccessToken não injeta nem preenche parâmetro nenhum da
requisição: se o endpoint chamado declara um parâmetro como obrigatório, o cliente HTTP continua
tendo que mandar esse valor normalmente (URL ou corpo), mesmo que esse mesmo nome também exista no
Container do token. Os dois mecanismos não se misturam — o valor do token só aparece onde o script
pedir explicitamente, através de #ParamTokenValue ( ).
#ParamTokenValue ( )
Script Suportado: Somente Endpoint
Dentro do script (PL/SQL Script), #ParamTokenValue(campo) devolve o
valor do Container do token da requisição atual para o nome de campo indicado — funciona tanto em
SQL cru quanto dentro de um bloco Params: { }
(#CallNodeJS ou #Call).
Ao contrário da maioria das macros do motor, essa valida — não devolve vazio
silenciosamente:
AccessToken não enviado (nem header, nem URL), ou token inválido/inativo →
HTTP 400.
Token válido, mas o campo pedido não existe no Container dele →
HTTP 400 também.
A resposta pro cliente é sempre genérica ("AccessToken is required for this endpoint.") — não
revela qual campo faltou nem o nome interno usado no script. O detalhe completo (qual
#ParamTokenValue(campo), qual dos dois motivos) fica só no log do servidor.
Essa validação só entra em jogo pra quem realmente usa #ParamTokenValue no script —
um endpoint que não usa a macro em lugar nenhum nunca é afetado, mandando o header ou não.
O valor sai sem aspas — via de regra o Container guarda algo numérico (ex:
CHAVE_EMPRESA), então o padrão é não quebrar uma comparação numérica direta:
select m.marketplace, m.nome as descricao
from marketplace m
where chave_empresa = #ParamTokenValue(chave_empresa);
Se o valor guardado no Container for texto, force as aspas envolvendo com
#QuotedStr ( ):
where documento = #QuotedStr(#ParamTokenValue(documento));
Caso de uso — API simplificada por trás da API completa
O cenário que motivou o recurso: uma API de cadastro de pedido completa (a do ERP)
tem muitos parâmetros obrigatórios, entre eles CHAVE_EMPRESA — mas a API
compartilhada com o hub precisa ser simples, um JSON enxuto, sem pedir de volta um
dado que já está embutido no próprio token do cliente. A API simplificada resolve isso com um
#Call por baixo, pra API completa, levando o valor do token:
O endpoint criar_pedido continua exigindo chave_empresa normalmente
(a regra dele não muda) — só que, nesse caminho, quem fornece o valor é o próprio script, lendo do
token, em vez do cliente HTTP que chamou a API simplificada. Ver #Call ( )
para o comando completo (endereço sempre completo; parâmetros por posição ou por nome).
Authorized APIs
Por padrão um token não tem restrição nenhuma: uma vez válido e ativo, dá acesso a
qualquer API hospedada no servidor. Na tela Token Parameters, ao lado do Container,
existe uma lista de checkbox Authorized APIs — marcando uma ou mais, aquele token
passa a funcionar somente nas APIs marcadas; chamado contra qualquer outra, é
rejeitado com 403 Forbidden. Nenhum item marcado mantém o
comportamento de sempre (sem restrição).
É o mesmo mecanismo de Authorized APIs já usado pra restringir um usuário comum a
determinadas APIs — aplicado aqui ao token em vez de um login cadastrado.
Tipos de arquivo do Endpoint (JSON & Streams)
Um endpoint trafega dados em um de dois formatos: JSON, o formato
padrão, ou Stream, para arquivos binários. O formato é definido pelo campo
Format do endpoint e é sempre um só por endpoint.
JSON — formato padrão
Corpo da requisição e resposta em JSON: é o formato de todo o CRUD automático e da
grande maioria dos endpoints — registros, listas e estruturas aninhadas
(Object Array). Para arquivos pequenos, é possível enviar o conteúdo como
base64 dentro do próprio JSON, como detalhado mais abaixo.
Streams (arquivos)
Arquivos pesados — foto de produto, PDF de contrato, XML de nota fiscal — o ideal é gravar em pastas
fora do banco de dados. O recurso de Stream aplica essa regra de armazenagem: o
upload/download acontece em binário puro (nunca base64), gravando/lendo do disco do
servidor, enquanto o banco de dados guarda apenas um endereço leve (o nome do
arquivo).
Isso não significa que base64 esteja proibido. Enviar um arquivo (foto, PDF etc.) como base64 dentro
de um JSON é, aliás, um mecanismo comum para boa parte das imagens que circulam por aí. Se o cenário
pedir isso — por exemplo, guardar o conteúdo do arquivo dentro do próprio banco — o caminho é um
endpoint Format: Json padrão, igual qualquer outro, só com um cuidado: o campo que
recebe o valor precisa ser de um tipo que suporte o tamanho do conteúdo, idealmente text
ou blob. O Stream é a alternativa para quando esse arquivo é pesado e não deve
engordar o banco de dados nem trafegar em base64.
Format
Todo endpoint tem um campo Format, hoje sempre Json (o comportamento
padrão — corpo e resposta em JSON). Quando o Format é um dos tipos Stream abaixo, o
endpoint deixa de processar JSON: o corpo da requisição (ou da resposta) passa a ser o binário cru do
arquivo. Um endpoint sempre tem um único Format fixo — nunca aceita mais de um tipo de
arquivo na mesma chamada.
Imagens: JPG, PNG, WEBP
Documentos: PDF, DOCX, XLSX
Dados: XML, CSV, ZIP
Mídia: MP3, MP4, WEBM
Quatro verbos, quatro endpoints de verdade
GET, POST, PUT e DELETE do tipo
Stream continuam sendo endpoints normais, presos ao mesmo Menu.Método.Endpoint de sempre
— aparecem na árvore, ficam documentados no $help e herdam Show in $help
e AccessToken normalmente, sem nada especial de configurar. Não existe
uma rota genérica única para todos os arquivos — cada finalidade (foto de produto, contrato, recibo)
tem seu próprio endpoint, com seu próprio Format.
POST/PUT — envia o arquivo (upload/atualização).
GET — devolve o binário do arquivo (download).
DELETE — remove o arquivo físico.
Corpo da requisição
Sempre binário cru (Content-Type: application/octet-stream), nunca
base64 — evita o overhead de ~33% do base64 e não obriga carregar o arquivo inteiro na memória como
string. Corpo binário e corpo JSON nunca coexistem na mesma chamada: se um fluxo precisa dos dois
efeitos (arquivo + dado estruturado), são duas chamadas — sobe o arquivo primeiro,
depois grava o registro normal (JSON) referenciando o nome recebido.
Parâmetros — $filename e thumb
Como o corpo já é o arquivo, os parâmetros do endpoint são lidos da query string,
não do corpo — mesma regra que já vale hoje para parâmetro de endpoint GET.
POST /ApiName/api/Pedidos/Pedido_integrado/Post_img?$filename=abc123.jpg
GET /ApiName/api/Pedidos/Pedido_integrado/Get_img?$filename=abc123.jpg&thumb=false
$filename (e thumb, nos GET de imagem) são parâmetros próprios do
Stream: valem em todo endpoint Stream e
não precisam ser declarados na grid Parameters — que fica livre para
os parâmetros da pasta base (:nome), descritos mais
abaixo.
$filename — obrigatório. Nome do arquivo gravado, lido ou apagado;
no upload, não precisa ser igual ao nome do arquivo original no disco de quem está enviando (o
cliente escolhe livremente).
thumb — obrigatório nos GET de imagem (JPG, PNG e WEBP), como o
$filename: true devolve a miniatura (WebP, menor e mais
leve) e false devolve o original. A falta do parâmetro, ou qualquer outro valor,
resulta em HTTP 400. Não existe nos outros formatos nem no
POST, PUT e DELETE. Veja Miniaturas de imagem.
{raiz}\{format}\{pasta base}\{filename}
$formatnão entra na query string — já é uma propriedade do próprio
endpoint, resolvida a partir do cache de endpoints do servidor (o mesmo registro cacheado que já guarda
TYPE_ENDPOINT/CRUD_AUTO hoje) no momento em que a chamada chega. O cliente
nunca precisa informar de novo algo que o endpoint já sabe sobre si mesmo.
Pasta base do endpoint (Base folder)
Todo endpoint Stream pode ter uma Base folder, no editor do endpoint: uma subpasta,
dentro do Format, definida pelo desenvolvedor. É nela que o endpoint trabalha: o
GET busca o arquivo nessa subpasta, o POST e o PUT
gravam nela, e o DELETE apaga dela. Quem consome a API não escolhe nem
informa a pasta — ela faz parte do endpoint, e só o $filename vai na URL.
Base folder: Produtos
GET /api_name/api/Catalogo/Produtos/Listar_fotos?$filename=1001_AZ_01.jpg&thumb=false
→ {raiz}\JPG\Produtos\1001_AZ_01.jpg
Sem Base folder, os arquivos ficam direto na pasta do Format.
Parâmetro na pasta base
A Base folder aceita parâmetros no formato :nome, para pastas que
dependem de quem chama — por exemplo, uma pasta por empresa. Cada :nome precisa estar
declarado na grid Parameters do endpoint, a quantidade de parâmetros declarados precisa
bater com a quantidade de :nome da pasta, e todos precisam ser do tipo
string. Eles fazem parte da URL e aparecem no $help como parâmetros
normais do endpoint:
Base folder: Produtos\:namecompany
GET /api_name/api/Catalogo/Produtos/Listar_fotos?namecompany=acme&$filename=1001_AZ_01.jpg&thumb=false
→ {raiz}\JPG\Produtos\acme\1001_AZ_01.jpg
Se um parâmetro da pasta base não for enviado, a API responde
HTTP 400, sem tentar adivinhar uma pasta padrão.
Validação dos valores
Os valores de parâmetro da pasta base e o $filename viram parte do caminho no disco, por
isso o servidor valida cada um: não aceita separadores de pasta (/,
\), .., : nem os caracteres inválidos de nome de arquivo
(* ? " < > |). Qualquer valor fora disso resulta em
HTTP 400, e o arquivo nunca sai da raiz dos arquivos Stream.
O que o endpoint Stream não usa
Um endpoint Stream não executa script SQL e não tem Result: no editor, a área de
script e a aba Result ficam ocultas quando o Format não é Json. A grid
Parameters continua visível e serve aos parâmetros da pasta base.
Miniaturas de imagem
Endpoints Stream de imagem (JPG, PNG, WEBP) têm uma versão
menor e mais leve de cada arquivo, em WebP — ideal para listagens,
em que se mostra muita imagem de uma vez. O original continua como foi gravado (por exemplo, JPG).
Criação: a cada POST ou PUT de uma imagem, o
servidor grava o original e, em seguida, cria sozinho a miniatura na subpasta
thumbs da pasta do arquivo ({pasta base}\thumbs\{filename}.webp,
por exemplo Produtos\thumbs\1001_AZ_01.jpg.webp). Se a miniatura não puder ser criada, o
upload funciona normalmente e o motivo vai para o log.
Leitura:GET ...&thumb=true devolve a miniatura
(image/webp); thumb=false, o original. Se a miniatura ainda não existe,
o servidor devolve o original.
Exclusão: o DELETE apaga o original e a miniatura.
Tamanho e qualidade: no config.ini, seção [SW]:
ThumbMaxSide (lado maior em pixels, de 16 a 4000, padrão 200) e
ThumbQuality (qualidade do WebP, de 1 a 100, padrão 80).
Requisito: as miniaturas são criadas pelo Node.js. É preciso ter o
Node.js instalado e configurado no servidor (campo Node.js Path, ver
Node.js (integração)) e executar npm install uma vez na pasta
NodeBridge\Builtin, que instala o pacote de imagem usado pelo servidor. Sem isso, os
endpoints Stream de imagem funcionam normalmente, apenas sem miniaturas.
Conferir e criar as miniaturas que faltam
Imagens copiadas direto para a pasta (sem passar pelo servidor), ou gravadas antes de o Node.js estar
instalado, não têm miniatura. O método administrativo $StreamThumbnails varre todos os
arquivos de imagem da raiz dos arquivos Stream e cria as miniaturas que faltam. Aproveita para conferir
o formato real de cada imagem processada: um arquivo .jpg que na verdade
é PNG, por exemplo, aparece no relatório. Exige usuário administrador.
POST /$StreamThumbnails
POST /$StreamThumbnails?max=200 -- processa no máximo 200 imagens nesta chamada (padrão 500)
POST /$StreamThumbnails?all=true -- refaz também as miniaturas que já existem
Com muitas imagens, repita a chamada enquanto truncated for true. Sem o Node.js
configurado, ou sem o pacote de imagem instalado, o método responde
HTTP 503 com o motivo.
Exemplos de envio (upload)
Nos quatro exemplos abaixo, o arquivo vai puro no corpo da requisição (binário cru,
sem multipart/form-data e sem base64) e o $filename (mais os parâmetros da pasta base, se houver)
vai na query string, exatamente como descrito acima.
Convenção de pasta, uma subpasta por Format e, dentro dela, a pasta base do endpoint:
{raiz}\{format}\{pasta base}\{filename}
Exemplo: FileStream\pdf\Contratos\abc123.pdf (pasta base Contratos).
A raiz é configurada no servidor, em Server Control → aba
API Web Server → Stream Files Path. Em branco, vale a pasta
FileStream ao lado do executável. Com vários servidores atrás de um balanceador de
carga, todos devem apontar para a mesma pasta compartilhada (por exemplo, um
caminho de rede), com permissão de leitura e gravação para o serviço do CrudSQLServer: assim
cada arquivo existe uma única vez, e qualquer servidor o encontra. A raiz só muda com o servidor
parado, depois do Save.
Quem grava a referência
Não existe um "registro de arquivos" próprio do framework (sem tabela automática, sem ID gerado pelo
servidor). O endpoint devolve o nome gravado; o script SQL do endpoint decide em qual
tabela/coluna guardar essa referência — mesmo controle total que o developer já tem sobre qualquer
outro dado. Recomendado gravar o filename numa coluna (ex: abc123.pdf); a pasta é a
Base folder do endpoint, então não precisa ser guardada junto.
Content-Type e exibição no navegador
No GET, o servidor decide automaticamente — a partir do Format do
próprio endpoint — o Content-Type (mime type correto) e o
Content-Disposition:
inline (abre direto no navegador) — Imagens, PDF, e os formatos de Mídia
(MP3/MP4/WEBM).
attachment (força download) — XML, CSV, ZIP, DOCX, XLSX: o navegador não sabe
exibir esses formatos sozinho.
Não existe campo de configuração pra isso — é sempre derivado do Format, sem exceção manual por
enquanto.
Listar arquivos (galeria/portal)
Para listar/filtrar arquivos (por pasta, nome, ou qualquer critério de negócio), use um endpoint
Format: Json normal, consultando a mesma tabela onde o script de upload gravou
o filename — filtro, paginação, join, tudo funciona igual a qualquer
outra listagem. O cliente recebe o array de nomes e monta a URL de cada imagem usando o padrão
Get_xxx?$filename=... descrito acima (nos endpoints de imagem, com &thumb=true ou
&thumb=false). Para o script do endpoint Json montar
esse endereço sem escrevê-lo à mão em cada SQL, use o #StreamUrl ( ).
Público vs. privado
Para delegar acesso a uma plataforma, usuário ou portal de integração sem usar usuário e
senha, mas ainda exigindo algum nível de autenticação, cria-se um
AccessToken — o consumidor manda esse token no header
AccessToken ou embutido na própria URL (?AccessToken=..., ver
AccessToken) em vez de mandar credencial — igual já funciona hoje
para os demais endpoints da API. Como a URL também aceita o token, isso já cobre até um
<img src="...?AccessToken=..."> cru de HTML, sem precisar de código que monte
um header.
Para o caso em que nenhuma autenticação é possível — nem usuário/senha, nem
token nenhum na chamada — o endpoint tem a opção Anonymous Access. Ligada, o
endpoint fica livre de qualquer autenticação, um GET direto na URL já funciona. É o caso de
integrações com plataformas que buscam um arquivo por um mecanismo totalmente padrão, sem
nenhuma customização possível na URL usada — por exemplo, o próprio motor de importação de uma
plataforma batendo direto numa URL fixa que ela mesma gerou, sem opção de acrescentar parâmetro
nenhum.
Resumindo os três níveis, do mais aberto ao mais restrito: Anonymous Access (zero
autenticação) → AccessToken (autenticação sem usuário/senha, por header ou
por URL) → usuário/senha (autenticação completa, o padrão de qualquer
endpoint hoje).
Endpoint
O Endpoint é o resultado final do CrudSqlBuilder: é a rota REST pela qual um site, um
aplicativo ou outro sistema consome a sua API. A finalidade do programa é
transformar os metadados de um banco de dados — e as consultas SQL sobre ele — em endpoints
REST prontos para uso — você descreve a
entidade ou escreve a consulta, e o servidor publica a rota, cuidando de autenticação, pool de conexão
e log.
Um endpoint responde aos métodos GET, POST, PUT e DELETE e pode ser criado de duas
formas:
Crud Automático — a partir do Descriptor, o sistema gera sozinho os endpoints de
Insert, Update e Delete. A estrutura padrão desses
endpoints não é editável manualmente.
Endpoint manual — você define o endpoint e escreve o script que ele executa, para
consultas e regras de negócio que o CRUD padrão não cobre.
Como regra geral, os endpoints utilizam parâmetro de script SQL para processamento,
principalmente nos métodos GET, POST e PUT, conforme o
tipo de requisição e a estratégia adotada.
Parâmetros / funções principais do Endpoint:
Show in $help: define se o endpoint aparece na documentação da API
($help). Apenas endpoints com True são expostos na documentação.
Processing Mode: define como o endpoint é processado — com o CRUD
automático gerado pelo servidor a partir dos metadados, ou com um PL/SQL
Script escrito no editor. O script permite a integração com processamento externo (ex.:
Node.js, por #CallNodeJS) e com outras APIs, por
#Call ( ).
Anonymous Access: quando ligado, dispensa qualquer autenticação — nem
usuário/senha, nem AccessToken — pra chamadas nesse endpoint
específico. Ver Tipos de arquivo (JSON & Streams) para o caso de uso original que
motivou o campo.
Cache: por quanto tempo a resposta de um GET fica guardada em memória no servidor
(No cache, Cache 10s, Cache 30s,
Cache 60s, Cache 5min, Cache 1h ou
Cache forever (static data)). Ver Cache de resultado.
Anonymous Access serve para qualquer situação em que o consumidor não precisa se autenticar para
acessar o JSON ou o arquivo do Stream. Um exemplo: um endpoint de cotação de preços
exposto pra fornecedores ou parceiros consultarem o preço atual de um produto direto por URL —
quando o consumidor é uma planilha, um robô de cotação ou o sistema de um terceiro que só sabe
montar uma URL e ler o retorno, sem nenhum mecanismo de autenticação disponível do lado dele. Vale
também para leitura de arquivo binário — foto de produto, PDF de
contrato ou XML de nota fiscal — exposta para consulta direta por URL,
como no caso de uma plataforma de marketplace ou de um robô de importação.
Parâmetros de entrada
Cada endpoint declara sua própria lista de Parâmetros (nome, tipo, obrigatoriedade,
máscara etc.). Um parâmetro pode ser usado de duas formas dentro do SQL/script:
Obrigatório (Required): se não vier na chamada, a API responde
HTTP 400 antes mesmo de tentar rodar qualquer SQL.
Opcional, dentro de um bloco [...]: todo o trecho entre colchetes só
entra no SQL quando o parâmetro é enviado; se não vier, o bloco inteiro some do
comando final — não vira condição vazia nem gera erro.
Cuidado comum: se o SQL já tem sua própria cláusula WHERE fixa, o bloco
condicional que acrescenta mais um filtro precisa começar com and, nunca com
where — duas cláusulas WHERE no mesmo comando é erro de sintaxe do banco assim
que o parâmetro é enviado (o [where ...] só deve ser usado quando ele é a
única condição do comando).
-- ERRADO se o SELECT já tem "where" fixo:
select ... from produtos p where p.tipo_prod='MP'
[ where p.produto=:produto ]
-- CERTO:
select ... from produtos p where p.tipo_prod='MP'
[ and p.produto=:produto ]
Todo valor de data/hora enviado como parâmetro segue o padrão ISO 8601.
Validação: antes do script começar a rodar, todo parâmetro efetivamente enviado na
chamada é validado contra o tipo declarado no endpoint:
INTEGER / BIGINT — precisa ser um número inteiro válido;
DECIMAL / DOUBLE PRECISION — precisa usar . como separador decimal
(, não é aceito);
STRING / TEXT — não pode exceder o tamanho máximo declarado do campo;
DATE / DATE TIME — precisa estar no padrão ISO 8601 (ver acima).
Se houver mais de uma violação na mesma chamada, todas são retornadas juntas em uma única resposta
HTTP 400 — o cliente não precisa corrigir um erro por vez.
Resultado (Result)
A lista de campos declarada em Result filtra o que realmente sai na resposta — qualquer
outro campo trazido pelo SQL e não declarado ali é descartado (ver
select / select:alias para o caso dos endpoints em
PL/SQL Script). Um campo do Result pode ser do tipo OBJECTARRAY,
apontando para uma declaração de ObjectArray — nesse caso o valor final desse
campo é sempre um array JSON (nunca escalar) e nunca é omitido da resposta, mesmo vazio.
Chamando outro endpoint (#Call)
Dentro do PL/SQL Script de um endpoint, é possível chamar outro endpoint
internamente — da mesma API ou de outra API cadastrada — reaproveitando sua lógica (validações,
escrita, consulta) em vez de duplicá-la. Ver #Call / #Call:alias
para a sintaxe completa, sempre endereçada como <nomedaapi>.api.<caminho>, e
o uso de #Call:alias para capturar o resultado (linha a linha, inclusive com
#For depois).
Exemplo de Post — Tabela principal + Sub-Table
O insert da tabela principal ganha um alias e um
#RETURN quando existe uma Sub-Table dependente dele — o valor retornado (a PK gerada) é
reaproveitado como FK em cada linha inserida no #For. Exemplo simplificado, com poucos
campos, de um endpoint PRODUTO com Sub-Table PRODUTO_SKU
(Object Array grade_produto):
Exemplo de Put — Update Strategy "atualiza e insere só os novos"
Nessa estratégia, cada linha da Sub-Table chega marcada pela própria PK: se ela vier vazia, é uma
linha nova (insere); se vier preenchida, é uma linha existente (atualiza). O que existia no banco e não
veio mais no Object Array é excluído — sempre com a exclusão restrita à FK do
registro principal, pra nunca atingir linhas de outro registro:
update PRODUTO set NOME = :NOME, MARCA = :MARCA, PESO = :PESO, ATIVO = :ATIVO
where ID_PRODUTO = :ID_PRODUTO;
delete from PRODUTO_SKU where ID_PRODUTO = :ID_PRODUTO
and ID_PRODUTO_SKU not in #AsInList( :grade_produto.ID_PRODUTO_SKU );
Esse delete é o que remove os registros que não foram enviados no
body — compara a PK de cada linha que já existe no banco (ID_PRODUTO_SKU) contra a
lista de PKs que vieram no Object Array desta chamada (#AsInList): tudo
que existia e não está mais na lista é apagado. Sempre com ID_PRODUTO = :ID_PRODUTO
travando o escopo no registro principal — sem isso, apagaria linhas de outros produtos também.
Exemplo de Put — Update Strategy "remove tudo e reinsere"
Já nessa estratégia não existe comparação linha a linha: todo o conteúdo atual da Sub-Table é apagado
(sempre restrito à FK do registro principal) e o Object Array inteiro é reinserido do
zero, sem se preocupar em preservar a PK das linhas antigas:
update PRODUTO set NOME = :NOME, MARCA = :MARCA, PESO = :PESO, ATIVO = :ATIVO
where ID_PRODUTO = :ID_PRODUTO;
delete from PRODUTO_SKU where ID_PRODUTO = :ID_PRODUTO;
#for :grade_produto
do
#begin
insert into PRODUTO_SKU ( ID_PRODUTO, COR, TAMANHO, SKU, ESTOQUE_MIN )
values ( :ID_PRODUTO, :COR, :TAMANHO, :SKU, :ESTOQUE_MIN );
#end;
Exemplo de Delete — Tabela principal + Sub-Table
No delete, a ordem é invertida em relação ao post/put: a Sub-Table é apagada primeiro (restrita à FK
do registro principal), e só depois o registro principal — sempre respeitando a dependência de chave
estrangeira:
delete from PRODUTO_SKU where ID_PRODUTO = :ID_PRODUTO;
delete from PRODUTO where ID_PRODUTO = :ID_PRODUTO;
Cache de resultado
O cache guarda em memória, no servidor, a resposta de uma consulta. Enquanto ela for
válida, quem chamar o mesmo endpoint com os mesmos parâmetros recebe a resposta pronta, sem ir ao
banco de dados. Consultas pesadas, chamadas várias vezes por minuto (listas de uma tela, combos,
painéis), deixam de repetir o mesmo trabalho no banco a cada acesso.
O cache é configurado em dois níveis, cada um com a sua própria opção:
Endpoint — guarda a resposta completa do endpoint. Vale somente para
GET; POST, PUT e DELETE sempre executam no banco.
ObjectArray — guarda o resultado da consulta do array.
Útil quando o endpoint principal precisa ser sempre atual, mas o array é estável (ex.: lista de
municípios, tabelas fiscais, dados do IBGE, tokens).
Opções
No CrudSqlBuilder, o Endpoint e o ObjectArray têm um campo Cache com as opções
abaixo. O padrão é No cache: o cache só é usado quando você escolhe uma das outras
opções no Endpoint ou no ObjectArray.
Opção
Validade da resposta guardada
Quando usar
No cache (padrão)
Nenhuma — toda chamada vai ao banco
Dados que precisam estar sempre atualizados no exato momento da chamada, como saldo de estoque ou status de um pedido logo após uma gravação.
Cache 10s
10 segundos
A maioria dos endpoints: o dado continua praticamente atual e as chamadas repetidas em sequência (cliques seguidos em pesquisar, telas que disparam várias consultas iguais) não voltam ao banco.
Cache 30s
30 segundos
Dados que mudam com frequência, mas suportam alguns segundos de atraso.
Cache 60s
60 segundos
Listas e consultas do dia a dia: clientes, produtos, fornecedores, categorias.
Cache 5min
5 minutos
Dados que mudam poucas vezes ao dia, como cadastro de clientes, categorias e tabelas de preço.
Cache 1h
1 hora
Dados que raramente mudam ao longo do dia.
Cache forever (static data)
Até a API ser recarregada ou sincronizada
Dados estáticos, que praticamente não mudam (exemplos abaixo).
Exemplos de dados estáticos para Cache forever (static data): municípios, estados
(UF), países, CFOP, NCM, CST/CSOSN, unidades de medida, tabelas de alíquota, formas de pagamento e
feriados nacionais.
Como a resposta é identificada
Cada resposta guardada é identificada pelos parâmetros da chamada — o mesmo endpoint
chamado com parâmetros diferentes gera respostas guardadas diferentes. Quando o script usa dados de
quem está chamando (#UserAuth ( ), AccessToken
ou #ParamTokenValue ( )), o usuário ou o token também faz parte da identificação, para que
um consumidor nunca receba a resposta de outro.
No ObjectArray, a identificação é o SQL final do array, já com todos os valores
substituídos — inclusive os que vêm de um :alias.campo do script.
Quando o cache não é usado
Mesmo com uma opção de cache escolhida, o servidor executa direto no banco nos casos abaixo, para que
nenhuma ação deixe de acontecer:
Métodos POST, PUT e DELETE não possui cache (o cache do endpoint vale só para GET).
A opção Cache forever (static data) também não é aceita quando o script usa macros de
data (#Date ( ) / #DateTime ( ) e as demais), ou dados de quem está
chamando — a resposta ficaria presa ao dia ou ao usuário da primeira chamada. Pelo mesmo motivo, essa
opção não é aceita em consultas que usam funções de data do próprio banco, como CURRENT_DATE ou
CURRENT_TIMESTAMP.
Validade e atualização
Ao vencer o tempo da opção escolhida, a próxima chamada vai ao banco e a resposta guardada é
renovada.
Toda resposta guardada é descartada quando a API é recarregada ou sincronizada — alterar um endpoint
nunca deixa uma resposta antiga em uso.
Chamadas iguais que chegam ao mesmo tempo, com o cache vazio ou vencido, consultam o banco
uma única vez; as demais aguardam e recebem a mesma resposta.
Existe um limite de memória para as respostas guardadas. Ao atingi-lo, as respostas
menos usadas são descartadas primeiro.
Atenção: o cache só enxerga as gravações feitas pela própria API. Se o ERP ou outro
sistema grava direto no banco de dados, a resposta pode chegar com atraso de até o tempo
da opção escolhida (por exemplo, 10 segundos com Cache 10s). Para dados que precisam refletir essas gravações na hora, use
No cache.
No log
Quando a resposta vem do cache, o log da requisição registra o evento CACHE_HIT, com o
tempo da chamada e a idade da resposta guardada, no lugar dos eventos de consulta ao banco.
Object Types and Operations
O framework organiza os objetos em quatro tipos principais: Descriptor,
Endpoint,
ObjectArray e Sub-Table. Cada um
possui
um
papel
específico na arquitetura e define como os dados são expostos e manipulados na API.
A tabela abaixo apresenta quais operações cada tipo de objeto suporta:
Object Type
GET
POST
PUT
DELETE
Endpoint
✅
✅
✅
✅
ObjectArray
⚠️
❌
❌
❌
Sub-Table
❌
⚠️
⚠️
⚠️
Legenda:
✅ Operação suportada diretamente pelo objeto
❌ Operação não suportada
⚠️ Disponível apenas de forma indireta, geralmente como parte de um endpoint
O Endpoint é o único responsável por operações de escrita
(POST, PUT e DELETE), sendo o ponto central de controle da API.
Os demais objetos atuam como estruturas auxiliares, participando apenas de operações de leitura,
normalmente vinculadas a um endpoint.
Descriptor
Manual Process Descriptor
O Manual Process Descriptor não possui processos automáticos: ele exige que 100% da
implementação seja feita manualmente pelo desenvolvedor, no modelo “faça você mesmo”.
Ele é utilizado quando o processo exige regras específicas, manipulação de múltiplas entidades ou
comportamentos que
não podem ser atendidos automaticamente pelo Crud Automático.
Nesse tipo de Descriptor:
Toda a lógica de processamento é de responsabilidade do desenvolvedor
Operações como inclusão, alteração e exclusão devem ser implementadas manualmente
É necessário o uso de script SQL, utilizando a linguagem do framework (semelhante
ao
PL/SQL)
Esse modelo é indicado para cenários mais complexos, como:
Processos que envolvem múltiplos cadastros
Regras de negócio específicas
Operações que exigem controle total sobre a persistência dos dados
Crud Automático
O Crud Automático representa uma entidade do banco de dados e contém a lista de
campos
(fields) correspondentes à tabela.
Esse tipo de Descriptor possui suporte automático a operações de:
Insert
Update
Delete
Não sendo necessário declarar script SQL, pois o framework gerencia essas operações de
forma
automática.
⚠️ Importante
O processamento de sub-table é realizado automaticamente pelo framework, para as
tabelas
declaradas
No contexto de Update (PUT), o comportamento é definido pelo campo Update Strategy:
Update In-Place: atualiza os registros existentes sem executar remoção completa, preservando alterações pontuais. Também insere novos registros quando a chave primária da sub-table for enviada como null.
Replace (Delete + Insert): remove os registros existentes com base na Foreign Key e insere novamente os registros enviados.
Com isso, você pode escolher a estratégia mais adequada para cada cenário, equilibrando consistência,
performance e regra de negócio.
Fields Descriptor
Fields Descriptor - flags
Primary key:
Parâmetro obrigatório e pode existir apenas um único Primary Key.
Required:
A API vai exigir que seja passado um valor para os campos com este parâmetro
marcado.
Custom Fields:
Permite adicionar campos que não pertencem à tabela principal, sendo utilizado principalmente em
Crud Automático.
Campos marcados como Custom Field indicam aos endpoints Insert
e
Update que o campo não existe fisicamente
na tabela do banco de dados. Esses campos são usados para exibição e manipulação de dados
relacionados,
geralmente em sub-table, sem necessidade de vínculo direto por foreign
key, ou quando se trata de um array de dados, por exemplo, itens de
um pedido.
Os campos Custom Field são representados como um array no resultado do JSON do endpoint
GET.
Invisible:
Serve para orientar a construção do formulário no front-end.
ReadOnly:
Serve para orientar a construção do formulário no front-end.
NoDuplicate:
Valida se já existe um registro na tabela com o mesmo valor do campo antes de seguir com a inclusão e alteração dos dados.
FieldType
O framework oferece suporte aos principais tipos de dados: Integer, Varchar, Date, DateTime,
Currency, Double Precision, Boolean, ObjectArray e Blob.
Campos do tipo Blob são indicados para conteúdos maiores, como textos extensos e dados
binários.
Para integração via JSON, o uso de Base64 no transporte e
armazenamento de Blob é uma orientação recomendada, não uma obrigatoriedade.
Essa orientação também se aplica a componentes como TextArea, quando utilizam Blob
internamente.
Default Value
Serve para preenchimento automático do campo quando nenhum valor for enviado na API.
Observações por FieldType:
FieldType Boolean = true ou false
FieldType String = não precisa colocar entre aspas
FieldType Date = mm/dd/yyyy
FieldType DateTime = mm/dd/yyyy hh:mm:ss
FieldType Double Precision ou Currency = ex.: 121.90
Manual Input
Quando este campo estiver preenchido, ele não será exposto na API; nesse caso, a aplicação grava automaticamente o valor definido nele.
Note
É usado para informar detalhes do campo na documentação da API gerada automaticamente. Esse campo
suporta HTML, permitindo exibir o conteúdo já formatado.
Status Pedido 1 - Aprovado 2 - Em Separacao 3 - Despachado 4 - Entregue
Associate Object
O Associate Object possui funções específicas e essenciais. Basicamente ele permite
vincular
um
campo a um
Dataset, criar listas de opções ou arrays de registros.
Os objetos declarados servem para apoiar a construção do frontend:
alguns desses mecanismos de Associate Object são apenas uma marcação de
como o campo deve ou pode ser representado na interface.
SimpleDropdown:
Lista de opções simples, indicada para seleção de valores fixos e
pré-definidos.
DataDropdown:
Vincula um campo a uma tabela do banco de dados, permitindo a seleção de um único registro. Por
exemplo,
no cabeçalho de um pedido é necessário selecionar um cliente.
Embora a lista apresente todos os clientes disponíveis, o campo ficará vinculado a apenas um cliente
específico.
Campos configurados como DataDropdown utilizam consulta auxiliar,
que é
retornado GET na forma de um array, contendo os registros
disponíveis para seleção.
Esse dataset normalmente é utilizado apenas para consulta e navegação pelo client.
DatalookupDropdown:
Este campo sinaliza ao desenvolvedor do frontend que os registros podem ser obtidos
por
meio de consultas dinâmicas e segregadas,
caso seja necessário atualizar ou refinar os dados apresentados, utilizando filtros enviados
como
parâmetros ao servidor.
Campos configurados como DatalookupDropdown utilizam consulta auxiliar,
que é
retornado GET na forma de um array, contendo os registros
disponíveis para seleção.
Esse dataset normalmente é utilizado apenas para consulta e navegação pelo client.
Dessa forma, o client pode construir uma interface de pesquisa ou pré-filtro,
enviando os
parâmetros desejados ao consulta auxiliar associado ao campo e realizando um
GET
separado para
recuperar os registros correspondentes.
CheckBox:
Define um campo do tipo boolean. Quando a base de dados possui suporte nativo a
esse
tipo, o valor é mantido diretamente como boolean.
Em bases de dados que não possuem suporte a campos booleanos, o valor pode ser armazenado como
string, utilizando "T" (True) ou "F" (False).
Nesses
casos,
o framework realiza automaticamente a conversão para True ou False
nos
retornos dos endpoints GET.
Nos endpoints POST e PUT, o valor deve ser enviado como
boolean, mantendo compatibilidade com o padrão de mercado, independentemente da
forma
como
o campo é armazenado no banco de dados.
Date & DateTime:
Define o formato de campos de data ou data e hora, garantindo compatibilidade com o padrão
utilizado em
aplicações.
Campos do tipo Date, o padrão esperado é: yyyy-mm-dd
Campos do tipo DateTime, o formato segue o padrão ISO-8601,
incluindo
data, hora, minuto e segundo: yyyy-mm-ddThh:mm:ss
TextArea:
Define que o campo é do tipo BLOB. Campos desse tipo são utilizados para
armazenar
textos
de maior tamanho.
Nos endpoints da API, os campos BLOB devem ser representados no formato
Base64, garantindo a correta transmissão dos dados em estruturas
JSON. Trata-se de uma orientação de como esse tipo de campo deve ser tratado —
a conversão para Base64 não é feita automaticamente pela ferramenta, sendo de
responsabilidade de quem implementa a leitura/gravação do campo.
ObjectArray:
Permite representar um conjunto de registros relacionados, retornado no formato
de
array. Esse tipo de objeto é normalmente utilizado para trabalhar com
listas de
dados vinculadas independente, mas que precisam compor a tela em questão.
No client, pode ser representado por diferentes componentes, como botões de ação, grids ou
subformulários, permitindo a inclusão, edição ou remoção de registros da lista.
Esse recurso é frequentemente utilizado para estruturar sub-listas com campos do tipo
Dropdown, possibilitando a construção de relacionamentos e composições de dados.
O campo declarado nesse formato deve ser do tipo Custom Field, ou seja, não
deve
existir fisicamente na tabela principal do banco de dados, sendo utilizado apenas para
representar e manipular dados relacionados.
Checking Crud
Trata-se de um mecanismo que permite validar vínculos de um registro com outras tabelas
do banco de dados. Ele pode ser utilizado tanto em operações de exclusão,
quanto
em
alterações ou inclusões de novos dados.
Por meio de parâmetros declarados com base nas tabelas e campos relevantes, é possível
configurar as regras de validação (checking). O uso mais comum ocorre durante o
processo de
exclusão, onde é necessário verificar se o registro possui relacionamentos com outras
tabelas,
impedindo a remoção indevida, apresentando uma mensagem mais amigável ao usuário.
Além disso, é possível validar se o registro que está sendo inserido ou alterado já
existe,
ou
se a modificação é permitida conforme as regras definidas.
Sub-Table
A Sub-Table é utilizada para definir os campos que serão inseridos na tabela relacionada,
além da estratégia de processamento aplicada no update. Esse mecanismo é exclusivo do Crud
Automático — endpoints resolvidos por PL/SQL Script não usam Sub-Table;
nesse caso, a gravação relacionada é responsabilidade do próprio script.
Parâmetros principais da Sub-Table:
Sequence: define a sequência de inclusão e exclusão de dados, conforme a estratégia de
update selecionada.
Empty Data Source: define o comportamento quando os dados da fonte não são
enviados na API. Pode aceitar a ausência de dados ou bloquear o endpoint com rejeição. Esse parâmetro
utiliza um campo de apoio para mensagem de validação (Validation Message/Warn).
Update Strategy: define se a aplicação vai remover todos os dados e reinserir com base
no payload enviado, ou se vai atualizar os registros existentes e inserir apenas os novos.
Relacionando os campos com a fonte de dados
Cada campo da Sub-Table precisa ser relacionado com o campo correspondente vindo da
fonte de dados enviada (o Object Array/Descriptor usado como
Data Source, ver tópico ObjectArray) — é essa ligação que diz
de onde vem o valor de cada coluna no momento de gravar.
Foreign Key ↔ Primary Key
A Sub-Table precisa ter um campo marcado como FK (chave estrangeira),
relacionado com a PK (chave primária) da tabela principal — é essa relação que vincula
cada linha gravada na tabela relacionada ao registro correspondente da tabela principal, e também é usada
para localizar/excluir as linhas relacionadas quando a estratégia de update remove e reinsere os dados.
ObjectArray
O ObjectArray tem como premissa definir os campos de uma estrutura em formato
array. Ele pode ser declarado em Descriptor,
Endpoint ou dentro de outro ObjectArray.
Em descriptores do tipo Crud Automático, o relacionamento de campos usado pela
estrutura de Sub-Table consome o body com o conteúdo do ObjectArray.
Esse objeto é fundamental para inserção de registros em sub-table, pois, junto com o
Descriptor, pode ser utilizado como Data Source (loop) no vínculo
dinâmico das Sub-Tables.
Declaração própria (SQL + Parâmetros)
Cada ObjectArray tem sua própria consulta SQL e sua própria lista de
Parâmetros — independentes do Endpoint que o usa. As mesmas
regras de parâmetro do endpoint valem aqui (Required e bloco [...] opcional),
com uma diferença importante na obrigatoriedade: se o array exige um parâmetro que o endpoint que o
referencia não declara/envia, o erro é HTTP 500
(falha de quem montou o endpoint), não HTTP 400 — quem chamou a API
não tinha como adivinhar que precisava enviar esse valor.
Quando é preciso listar o ObjectArray de acordo com consultas internas do script (em
vez da consulta declarada nele por padrão), existe a macro
#OverrideObjectArraySql { }, que sobrescreve o SQL do
array no momento da execução.
ObjectArray dentro de outro ObjectArray
Um campo do Result (ou dos Parâmetros) de um ObjectArray
também pode ser do tipo OBJECTARRAY, apontando para outro ObjectArray —
carregado de forma recursiva, exatamente como acontece quando o campo aninhado está no Result
de um Endpoint. Isso permite estruturas de array dentro de array,
com quantos níveis forem necessários.
Cache
O ObjectArray tem o seu próprio campo Cache, independente do
endpoint que o usa — por exemplo, o endpoint sem cache e o array com Cache 60s. A
resposta guardada é identificada pelo SQL final do array, já com os valores substituídos. Ver
Cache de resultado.
Como o array chega no Result de um GET
Quando um ObjectArray é declarado como campo do Result de um endpoint
(ver Endpoint → Resultado), sua consulta roda sempre uma única vez
por requisição — nunca uma vez por linha do resultado principal. O que muda é como esse
resultado é distribuído entre as linhas:
Como a consulta do ObjectArray roda uma única vez, nos casos em que ela não recebe do
endpoint principal um parâmetro capaz de filtrar direto pra cada linha, a lista trazida pode conter
registros de vários itens misturados — que precisam ser correlacionados com a linha
certa do select principal antes do JSON final ser montado. É pra isso que existe o parâmetro
#JoinObjectArray: relacionar a lista de campos
retornados pelo array com a lista do select principal.
Sem #JoinObjectArray: o array inteiro (todas as linhas trazidas pela
consulta do ObjectArray) é colado em toda linha do select principal,
igual — útil quando o array não depende de cada linha (ex: uma lista fixa de opções, igual pra
qualquer registro).
Com #JoinObjectArray no select
principal, o resultado é agrupado por um campo de correlação e cada linha recebe só o grupo que bate
com ela — a opção certa quando o conteúdo do array muda de acordo com a linha (ex: as cores
disponíveis de cada produto).
PL/SQL Script
Além da geração automática de Insert, Update e Delete
a partir do Descriptor do tipo Crud Automático, o CrudSqlBuilder permite escrever a lógica de um endpoint
manualmente através de um script próprio, parecido com PL/SQL. É indicado para regras
que fogem do CRUD padrão: consultas compostas, gravações condicionais, laços de repetição sobre um
array recebido no corpo da requisição, chamadas encadeadas a outros endpoints, entre outros.
O script é composto por uma sequência de comandos, separados por ;.
O ; só é obrigatório quando existe outro comando na sequência; no último comando do bloco
ele é opcional.
Comentários são suportados nos dois formatos:
-- comentário de uma linha
/* comentário
de bloco */
Os comandos disponíveis são:
select / select:alias — consulta
INSERT / INSERT:alias — gravação, com retorno opcional via
#RETURN(campo)
#check(...) — validação de negócio
#Call / #Call:alias — chamada a outro endpoint
#iif(...) / #case(...) — decisão condicional
#For() ... #begin ... #end; — laço de repetição
Junto com os comandos, o script conta com recursos de apoio usados dentro do próprio texto SQL:
#Date()/#DateTime(), blocos condicionais [...], listas explícitas via
#AsInList()/#QuotedStr() e o registro de alias
(:alias.campo) — cobertos nos próximos tópicos.
Regra geral: tanto um parâmetro declarado no endpoint quanto um
alias criado dentro do próprio script são sempre invocados com os
dois pontos na frente — :PARAMETRO, :alias.campo. Essa regra
vale em qualquer lugar do script, inclusive dentro de argumentos de outras macros
(ex.: #AsInList(:alias.campo), #QuotedStr(:alias.campo)) — nunca existe uma
forma "sem os dois pontos" pra invocar um valor já existente.
Regra geral:( ) parênteses são usados pra argumentos de
uma macro/comando — #check(...), #Call(...), #For(),
#AsInList(...), #QuotedStr(...),
#CreateTemporaryTable:ALIAS(...), entre outros — inclusive quando o argumento é um corpo
JSON, como em #Call:alias endpoint({ campo = valor }). Já { } chaves são
usadas só quando o conteúdo é um bloco, não um argumento simples —
#JoinObjectArray{ },
#OverrideObjectArraySql:ALIAS{ }, e o conteúdo de cada branch de
#iif/#case (TRUE:{ ... }, ELSE:{ ... }). Os
dois símbolos não são intercambiáveis — cada comando aceita só o que está documentado
no seu próprio tópico.
Dentro de um corpo JSON (Params: { } do #CallNodeJS, ou o
corpo { } do #Call), cada par chave/valor
aceita tanto chave: valor quanto chave = valor — os dois funcionam
igual. = é o estilo recomendado quando o valor já começa com : (ex:
cep = :CEP), pra não repetir o mesmo símbolo duas vezes seguidas
(cep: :CEP funciona, mas fica mais difícil de ler).
Exemplo completo
Um único script combinando vários dos recursos acima: valida antes de fazer qualquer coisa
(#check), guarda o resultado de um select num alias pra
reaproveitar depois, percorre linhas de outra consulta atualizando registros relacionados
(#For + #DateTime()/#Date()), e monta colunas condicionais no
select final usando #iif/#case embutidos — inclusive um
#iif aninhado dentro de um branch do #case:
#check('SELECT COUNT(*) AS N from PEDIDO_VENDA where marktplace=:mkplace', N>0, 'Não tem pedidos nesta plataforma');
select:iten m.ativo, m.nome, m.chaveapi, m.marktplace, m.chave_empresa from marktplace m where marktplace = :mkplace;
#For()
select:itens_pv ppv.sku_manual, ppv.produto_pedidov, pedido_venda from produto_pedidov ppv
where ppv.pedido_venda=802024 do
#begin
update pedido_venda set data_cancelado=#DateTime() where pedido_venda=:itens_pv.pedido_venda;
update produto_pedidov set data_emissao=#Date(), sku_manual=:itens_pv.sku_manual
where produto_pedidov=:itens_pv.produto_pedidov;
#end;
select marktplace as MARKT, nome as DESC,
#iif( ( :iten.ativo='F' ) then
TRUE:{ 'X' },
ELSE:{ 'Y' }) as ativo,
:iten.ativo as ativo_original,
#iif(:iten.chaveapi then NULL:{ 'chave_api_null' as chave_api },
ELSE:{ 'chave_preenchida' as chave_api }),
#case(:iten.marktplace of
10:{ #iif(:iten.nome then null: {0}, ELSE:{ :iten.nome||'- entrouaqui -'||nome } ) as new_name },
ELSE:{ 'vazio' as new_name })
from marktplace
where marktplace = :mkplace ;
Repare que o alias iten (criado pelo select:iten lá em cima) é lido em
vários pontos do select final — inclusive dentro dos seletores dos #iif/
#case embutidos — e que #For/#begin/#end rodam suas
duas atualizações uma vez pra cada linha trazida por select:itens_pv.
Variáveis de Ambiente
Macros que devolvem um valor do contexto atual da requisição — data/hora do momento,
usuário autenticado, ou o resultado de um comando anterior guardado em memória — em vez de um valor
declarado explicitamente no script.
#Date ( ) / #DateTime ( )
Script Suportado: Endpoint e ObjectArray
Substituídos em tempo de execução pelo equivalente de data (ou data/hora) atual do banco configurado no
endpoint — não é preciso se preocupar com a sintaxe de cada banco. Os nomes batem de propósito com os
tipos DATE e DATE TIME já usados nos Parâmetros/
Result — mesma distinção, mesmo vocabulário.
INSERT INTO PEDIDOS (DATA_CRIACAO, STATUS) VALUES (#DateTime(), 'ABERTO');
INSERT INTO PEDIDOS (DATA_ENTREGA_PREVISTA, STATUS) VALUES (#Date(), 'ABERTO');
Banco
#Date()
#DateTime()
Firebird
CURRENT_DATE
CURRENT_TIMESTAMP
MySQL
CURDATE()
NOW()
PostgreSQL
CURRENT_DATE
NOW()
SQL Server
CAST(GETDATE() AS DATE)
GETDATE()
#UserAuth ( )
Script Suportado: Somente Endpoint
Substituído em tempo de execução pelo usuário autenticado que disparou a requisição
atual — útil pra gravar quem fez a ação, sem precisar declarar nenhum parâmetro novo no endpoint.
Diferente do #DateTime(), o valor já entra entre aspas (é texto, não uma
função/keyword do banco):
INSERT INTO LOG_ALTERACAO (USUARIO, DATA_HORA, ACAO)
VALUES (#UserAuth(), #DateTime(), 'ATUALIZOU_PEDIDO');
Se a requisição não tiver usuário autenticado, o valor gravado fica como texto vazio ('')
— nunca quebra o comando, mas também não inventa um valor.
#QuotedStr ( )
Script Suportado: Endpoint e ObjectArray
O motor já sabe identificar sozinho, na maioria dos casos, se um valor é texto (aspas) ou número (sem
aspas) — um parâmetro declarado no endpoint (:PARAMETRO), por exemplo,
já tem o tipo conhecido de antemão. #QuotedStr(...) existe pra forçar
aspas quando essa identificação automática não é possível — o caso mais comum é o campo de um
alias (:alias.campo): o resultado de um comando guardado em memória
nem sempre carrega um tipo declarado junto, então o motor pode não conseguir decidir sozinho se
aquele valor é texto ou número.
INSERT INTO LOG (DESCRICAO) VALUES (#QuotedStr(:cli.NOME));
INSERT INTO LOG (DESCRICAO) VALUES (#QuotedStr(:OBSERVACAO));
Também aceita envolver #ParamTokenValue ( ) — que sai sem aspas por
padrão (via de regra é numérico) — pra forçar o valor do Container como texto:
where documento = #QuotedStr(#ParamTokenValue(documento));
Da mesma forma, aceita compor com as Macros de Formatação
(#SetToFloat ( )/#SetToRound ( )/#SetToTrunc ( )/
#SetToInteger ( )) quando o destino é texto mas o valor precisa ser tratado como número
primeiro. Fora essas três origens (:parâmetro/:alias.campo, composição com
outra macro de formatação, ou #ParamTokenValue ( )), um valor literal solto não é
aceito — não faz sentido usar #QuotedStr(...) num valor que já é fixo.
[ ] Params Condicional
Script Suportado: Endpoint e ObjectArray
Trecho de SQL que é condicional ao parâmetro que ele referencia: o conteúdo declarado
dentro dos colchetes só entra no comando final se esse parâmetro for efetivamente enviado na chamada do
endpoint — não importa se veio pela URL ou pelo body. Se o bloco usa
mais de um parâmetro, todos precisam ter sido enviados; faltando qualquer um, o bloco
inteiro (colchetes e conteúdo) é descartado — sem gerar erro. Muito usado em filtros opcionais de um
WHERE:
Select m.marktplace, m.nome as descricao from marktplace m
[ where chave_empresa= :CHAVE_EMPRESA ]
Se CHAVE_EMPRESA for informado, o comando final fica:
Select m.marktplace, m.nome as descricao from marktplace m
where chave_empresa= 123
Se não for informado, o comando final fica:
Select m.marktplace, m.nome as descricao from marktplace m
Podem existir vários blocos [...] no mesmo SQL — cada um é avaliado de forma independente.
Macros Condicionais
Macros que decidem o que roda (ou o que entra no comando) de acordo com uma condição
— bloquear uma ação, escolher entre caminhos diferentes, ou incluir um trecho de SQL só quando um
parâmetro específico foi enviado.
#check ( )
Script Suportado: Somente Endpoint
Serve pra validar se uma ação é permitida antes de executá-la — roda o SQL informado,
avalia a condição contra o campo da primeira linha retornada e, se a condição for verdadeira,
bloqueia: interrompe o script e devolve HTTP 409 com a mensagem
declarada, sempre prefixada com "Access not permitted: ". Um bom exemplo é impedir o
cancelamento de um pedido que já teve nota fiscal emitida:
#check("select count(*) as N from PEDIDOS where PEDIDO = :PEDIDO and NOTA_FISCAL_EMITIDA = 'S'",
N>0,
'Pedido já possui nota fiscal emitida, cancelamento não permitido.');
Condições aceitas: =, <>, >, <,
>=, <=, comparando o campo retornado pelo próprio SQL do
#check contra um valor fixo.
Um parâmetro declarado no endpoint mas não enviado nesta requisição, quando usado no SQL do
#check, é tratado como null.
O ideal é colocar o #check logo no início do script — assim, se a ação
não for permitida, o endpoint já interrompe ali, sem gastar tempo rodando o resto do processamento.
#AsInList ( )
Script Suportado: Endpoint e ObjectArray
#AsInList(:alias.campo) monta uma lista de valores dentro de um IN (...), a
partir de um alias que teve mais de uma linha atribuída (ex.: dentro de um
#For). A referência ao campo segue a mesma regra de qualquer parâmetro/alias do motor —
sempre com os dois pontos:
select EMBALAGEM_CORREIO, DESCRICAO
from EMBALAGEM_CORREIOS
where EMBALAGEM_CORREIO in (#AsInList( :emb.EMBALAGEM_CORREIO ));
Por padrão o valor é tratado como número (sem aspas). Quando o campo é
texto, sinalize com #QuotedStr(...)
por dentro — ela não é uma função presa ao #AsInList, é uma macro independente que só
adiciona aspas (ver tópico #QuotedStr ( )):
select * from CLIENTES
where SITUACAO in (#AsInList( #QuotedStr(:cli.SITUACAO) ));
O #AsInList() só enxerga valores de um alias que já foi atribuído em algum comando
anterior do script (select:alias, INSERT:alias ... #RETURN, ou a própria linha
de um #For) — veja o tópico Alias.
Macros JSON
Macros para trabalhar com JSON dentro do script, cada uma resolvendo uma etapa diferente:
#SetFormatJson ( ) monta um texto JSON a partir de
um alias (inteiro, um campo só, ou uma projeção de campos escolhidos); e
#SetImportJsonArray ( ) importa um array
JSON pra dentro de um alias novo em memória, inclusive opção de importar apenas
um nó do array baseado na chave do nó dentro de um JSON maior. Os campos de data de um
JSON — que chegam em formatos muito diferentes — têm um macro para cada formato, reunidos em
Datas em JSON.
Na prática, as duas costumam trabalhar juntas num fluxo de integração:
#SetImportJsonArray localiza (se precisar) e materializa o array do retorno de um
#CallNodeJS como alias, e #SetFormatJson projeta só os
campos que o endpoint promete devolver no Result.
#SetFormatJson ( )
Script Suportado: Somente Endpoint
Serializa um alias já registrado (select:alias, #Call:alias,
insert:alias #RETURN(...) ou um OBJECTARRAY) direto em JSON — de uso geral,
não é exclusivo do #CallNodeJS: funciona em qualquer lugar do script. "Set" aqui é o conjunto de dados que você está pegando — a linha inteira,
um campo só, ou um recorte com só os campos que você escolher. Três formas:
Resumo: informar nome = valor existe pra permitir nomear o campo de
saída de acordo com o resultado esperado, quando o nome de origem (do alias) é diferente do nome
que o destino espera receber. Se a origem já tem o mesmo nome de campo e os mesmos campos (todos
eles, sem projeção), não é necessário nomear — nesse caso use direto #SetFormatJson(:alias).
Alias inteiro — #SetFormatJson(:alias)
Um array JSON com todas as linhas do alias, cada linha virando um objeto com todos os campos.
Uso comum: montar o Params de uma chamada Node com uma lista inteira de itens, sem
escrever campo a campo.
(TimeOut/Error/Success são obrigatórios em todo
#CallNodeJS — omitidos aqui só pra focar no Params, ver
#CallNodeJS ( ).)
Um campo só — #SetFormatJson(:alias.campo)
Um array JSON de objetos, um por linha, cada um só com esse campo — mesma forma da projeção de
vários campos (próximo tópico), só que com um único campo. A saída é sempre array de
objetos, independente de quantos campos foram escolhidos: assim, se um segundo campo for
adicionado depois, a forma do JSON não muda de repente (deixa de ser array de valores soltos e
vira array de objetos) — quem consome esse JSON do outro lado (um módulo Node, por exemplo)
nunca é surpreendido por uma mudança de formato.
Igual ao alias inteiro, mas com projeção de colunas — cada linha vira um objeto só
com os campos listados, na mesma ordem em que aparecem, ao invés de todos os campos do alias.
Nessa forma (dois ou mais campos), nome = é obrigatório pra cada
argumento — mesmo formato chave = valor já usado no Params do
#CallNodeJS. A chave define o nome do campo na saída; quando não
precisa renomear, repete o mesmo nome dos dois lados.
Numa integração vinda de fora (um JSON de marketplace, por exemplo — ver
#SetImportJsonArray ( )), o nome do campo de origem raramente
bate com o nome que você quer devolver — é aí que renomear de verdade importa:
Campo manual — misturando :alias.campo com valor fixo
Nessa mesma forma de vários campos, o valor depois do = não precisa vir sempre de um
alias — pode ser um literal fixo, aplicado igual em toda linha do array de saída.
Útil quando você está montando o JSON "na mão" e precisa incluir um campo que não existe na origem
(tipo, origem, flag, categoria...). Aspas são opcionais — texto sem aspas vira string do mesmo
jeito, número sem aspas sai como número no JSON:
Além de literal, o valor também pode ser um :param escalar — um parâmetro declarado no
próprio endpoint (query, corpo, ou já resolvido em :PARAM_TOKEN_VALUE), sem
.campo nenhum, porque não vem de linha de alias. Resolvido uma vez só, mesmo valor em
toda linha do array de saída — mesma regra de erro do Params do #CallNodeJS
(HTTP 400 se o parâmetro foi declarado mas não veio nessa
chamada, HTTP 500 se nunca foi declarado no endpoint):
Também dá pra montar o JSON inteiramente na mão, sem depender de alias nenhum — só literais, só
parâmetros, ou os dois misturados. Nesse caso não existe "linha de tabela" pra repetir, então não tem
ambiguidade: o resultado é sempre um único objeto, dentro do array (mesma forma de saída de sempre):
O valor de qualquer campo dessa forma também aceita #iif/#caseembutido — mesma resolução já usada no Valor do
#SetVarResult ( ): o branch escolhido vira o texto puro (literal,
:alias.campo ou :param), decidido em tempo de execução:
São as estruturas de decisão do motor de script — o mesmo papel que um if/switch tem em
qualquer linguagem de programação: definem qual caminho o sistema segue com base no dado de entrada.
Podem ser usados de duas formas: como comando de topo (o branch escolhido pode conter
qualquer outro comando: select, INSERT, #Call...) ou embutidos dentro de um texto SQL
(o branch escolhido vira texto puro, substituindo a expressão inteira).
Um parâmetro declarado no endpoint mas não enviado nesta requisição, quando usado na condição de um
#iif/#case, é tratado como null.
#iif — modo condição booleana
#iif( (:UF='SP') AND (:TIPO='PF') then
TRUE:{ update CLIENTES set ALIQUOTA = 0.07 where CLIENTE = :CLIENTE; },
ELSE:{ update CLIENTES set ALIQUOTA = 0.10 where CLIENTE = :CLIENTE; }
);
Reconhecido como expressão booleana quando o seletor contém =, <,
>, AND ou OR. A condição inteira é resolvida direto pelo banco
(um select case when (condição) then 1 else 0 end por baixo dos panos), então qualquer
operador de comparação válido em SQL funciona: =, <>, >,
>=, <, <=, combinados livremente com AND/
OR. Os rótulos dos branches, nesse modo, são sempre TRUE/ELSE.
Exemplo com OR — qualquer uma das condições sendo verdadeira já basta:
#iif( (:STATUS='CANCELADO') OR (:STATUS='DEVOLVIDO') then
TRUE:{ update PEDIDOS set BLOQUEADO = 'T' where PEDIDO = :PEDIDO; },
ELSE:{ update PEDIDOS set BLOQUEADO = 'F' where PEDIDO = :PEDIDO; }
);
O exemplo acima é comando de topo — cada branch é um comando completo (termina em
;). Mas o #iif também funciona embutido no meio de outro SQL:
o branch escolhido vira texto puro, substituindo a expressão inteira no lugar onde ela
está. Isso permite que o resultado do #iif seja só um pedaço de condição SQL, que entra ou
não no comando:
select p.produto, p.cod_produto, p.descricao1 as descricao, p.unidade_uso
from produtos p
where p.tipo_prod = 'MP'
#iif( (:Desativado='T') then
TRUE:{ and p.desativado = 'T' },
ELSE:{ }
);
Se DESATIVADO for enviado como 'T', o comando final ganha
and p.desativado = 'T' no WHERE; caso contrário, o branch
ELSE (vazio) não acrescenta nada — o filtro simplesmente não aparece no SQL, sem gerar
erro nem cláusula solta.
#iif — verificando se um valor foi informado (=null)
#iif( :OBSERVACAO=null then
TRUE:{ insert into LOG (MSG) values ('sem observação'); },
ELSE:{ insert into LOG (MSG) values (:OBSERVACAO); }
);
=null testa se o parâmetro ou :alias.campo está vazio (não informado) — é só
mais uma condição booleana, então os rótulos continuam sendo TRUE/ELSE,
iguais aos do modo condição booleana acima. Diferente de uma comparação SQL de verdade (onde
= NULL nunca dá certo, nem quando o valor realmente é nulo), o =null aqui é um
teste especial reconhecido pelo próprio motor de script — resolvido sem consultar o banco.
#case
#case( :STATUS of
'A':{ select 'Aberto' as DESCRICAO; },
'F':{ select 'Fechado' as DESCRICAO; },
ELSE:{ select 'Desconhecido' as DESCRICAO; }
);
Compara o valor do seletor contra cada rótulo declarado (texto ou número); o primeiro que bater tem seu
conteúdo executado (ou inserido no SQL, no uso embutido). Se nenhum bater, cai no ELSE,
quando declarado.
Assim como o #iif, o exemplo acima é comando de topo — mas o
#case também funciona embutido no meio de outro SQL, virando texto puro no
lugar da expressão. Útil quando o valor do parâmetro decide qual coluna/expressão usar, não qual
comando rodar:
ORDENAR_POR é um parâmetro de entrada do endpoint (enviado no body ou na
URL da requisição) — NOME e CODIGO, usados como rótulos no
#case, são valores possíveis desse parâmetro, não nomes de campo da
tabela:
ORDENAR_POR enviado
Coluna usada no ORDER BY
NOME
p.descricao1
CODIGO
p.cod_produto
qualquer outro valor, ou não enviado
p.produto (branch ELSE)
select p.produto, p.cod_produto, p.descricao1 as descricao
from produtos p
where p.tipo_prod = 'MP'
order by #case( :ORDENAR_POR of
'NOME':{ p.descricao1 },
'CODIGO':{ p.cod_produto },
ELSE:{ p.produto }
);
Aqui o resultado do #case é só o nome da coluna usada no ORDER BY — troca
conforme ORDENAR_POR, e cai em p.produto (branch ELSE) se o
valor não bater com nenhum rótulo declarado.
Aninhando #iif e #case
É permitido aninhar — o conteúdo de qualquer branch pode conter outro #iif/#case,
em qualquer combinação (#iif dentro de #iif, #case dentro de
#case, ou um dentro do outro), inclusive no uso embutido.
#iif dentro de #case — cada branch decide o valor de uma coluna usando seu próprio #iif interno:
#case( :Compras of
0:{ #iif( :Colecao=null then TRUE:{0}, ELSE:{ sum(PE9.quantidade) } ) as Itens_colecao },
1:{ #iif( :Colecao=null then TRUE:{ count(*) }, ELSE:{ sum(PE9.quantidade) * 0.9 } ) as Itens_colecao },
ELSE:{ 0 as Itens_colecao }
);
#iif dentro de #iif:
#iif( (:UF='SP') then
TRUE:{ #iif( (:TIPO='PF') then TRUE:{0.07}, ELSE:{0.09} ) as ALIQUOTA },
ELSE:{ 0.10 as ALIQUOTA }
);
#case dentro de #case:
#case( :MARKETPLACE of
10:{ #case( :STATUS of 'A':{ 'Aberto' }, ELSE:{ 'Outro' } ) as DESCRICAO },
ELSE:{ 'Desconhecido' as DESCRICAO }
);
select:alias dentro de um branch, usado por um #iif aninhado
No uso como comando de topo, o conteúdo de um branch é uma lista de comandos normal —
pode ter um select:alias seguido de um segundo #iif que já lê o alias recém-criado,
exatamente como aconteceria fora de qualquer #iif:
#iif( (:UF='SP') then
TRUE:{
select:produto CUSTO, MARGEM_PADRAO from PRODUTOS where PRODUTO = :PRODUTO;
#iif( (:produto.CUSTO>1000) then
TRUE:{ update PEDIDOS set DESCONTO = 0.05 where PEDIDO = :PEDIDO; },
ELSE:{ update PEDIDOS set DESCONTO = :produto.MARGEM_PADRAO where PEDIDO = :PEDIDO; }
);
},
ELSE:{ update PEDIDOS set DESCONTO = 0 where PEDIDO = :PEDIDO; }
);
O select:produto roda primeiro, grava o alias produto; o #iif
seguinte, no mesmo branch, já usa :produto.CUSTO e :produto.MARGEM_PADRAO
normalmente — os comandos dentro de um branch rodam em sequência, como se fossem parte do script principal.
select (sem alias) dentro de um branch
Um select sem alias dentro de um branch produz o resultado do endpoint
diretamente — o branch escolhido é quem decide qual consulta roda:
#iif( (:TIPO='PF') then
TRUE:{ select PRODUTO, DESCRICAO from PRODUTOS where TIPO_CLIENTE = 'PF'; },
ELSE:{ select PRODUTO, DESCRICAO from PRODUTOS where TIPO_CLIENTE = 'PJ'; }
);
O mesmo vale para #case:
#case( :STATUS of
'A':{ select PEDIDO, DATA_PEDIDO from PEDIDOS where STATUS = 'ABERTO'; },
'F':{ select PEDIDO, DATA_PEDIDO, DATA_FECHAMENTO from PEDIDOS where STATUS = 'FECHADO'; },
ELSE:{ select PEDIDO from PEDIDOS where STATUS = :STATUS; }
);
#LikeValue ( )
Script Suportado: Somente Endpoint
Verifica se um texto contém um trecho, para usar como condição no
#iif ( ) / #case ( ). É o equivalente ao Pos do Delphi, ao
contains/includes de outras linguagens. Importante: o
#LikeValue não é o LIKE do banco de dados. Ele compara valores que já estão no script:
parâmetros recebidos, campos retornados por consultas (alias) ou dados externos extraídos de JSON e XML,
como a resposta de um #CallHTTP.
#LikeValue(valor, 'trecho')
Útil principalmente com mensagens de plataformas externas, que costumam vir como texto livre, por
exemplo "Error: Pedido cancelado" ou "Erro pedido não pode ser alterado".
Regras
Regra
Comportamento
Maiúsculas e minúsculas
Não diferencia: "CANCELADO", "Cancelado" e "cancelado" batem com 'cancelado'
Acentos
Não diferencia: 'nao pode' bate com "não pode"
Curinga %
Não precisa: a busca já é por "contém", em qualquer posição do texto
Valor
:param, :alias.campo, variável do #SetVar ( ) ou #Http_Error(message)
Valor vazio ou não informado
Resultado falso (cai no ELSE)
Valor original
Não muda: a troca para maiúsculas e a retirada dos acentos servem só para comparar
Combinar
Aceita AND/OR com outro #LikeValue ou com outras condições
Exemplo — tratar a mensagem de erro da plataforma
#HttpNewUrl('https://api.exemplo.com.br/pedidos');
#HttpAddUrl('/' || :NUMERO);
#CallHTTP:PED(
TimeOut: 15 { #SetResponseCode(504); },
Error: {
#iif(#LikeValue(#Http_Error(message), 'cancelado') then
TRUE: {
update PEDIDO set CANCELADO = 'T' where NUMERO = :NUMERO;
#SetVarResult(SITUACAO = 'cancelado na loja');
#SetResponseCode(200);
},
ELSE: {
#iif(#LikeValue(#Http_Error(message), 'nao pode ser alterado') then
TRUE: {
#SetVarResult(SITUACAO = 'bloqueado para alteracao');
#SetResponseCode(409);
},
ELSE: {
#SetVarResult(SITUACAO = #Http_Error(message));
#SetResponseCode(502);
}
);
}
);
},
Success: {
#SetVarResult(SITUACAO = 'ok');
}
);
Se a mensagem contiver "cancelado" (em qualquer combinação de maiúsculas), o pedido é marcado como
cancelado e a resposta sai com 200. Se contiver "não pode ser alterado", com ou sem acento, devolve 409.
Qualquer outra mensagem é repassada com 502.
Exemplo — várias palavras com OR
#iif(#LikeValue(:MSG, 'cancelado') OR #LikeValue(:MSG, 'estornado') then
TRUE: { update PEDIDO set CANCELADO = 'T' where NUMERO = :NUMERO; },
ELSE: { }
);
<comando> :alias
Script Suportado: Endpoint e ObjectArray
Um alias é uma referência nomeada, guardada em memória durante a execução do script,
para o resultado de um comando. Serve para reaproveitar um valor obtido em um comando dentro de outro
comando mais adiante no mesmo script.
Onde um alias nasce:
select:alias ...; — a primeira linha do resultado;
INSERT:alias ... #RETURN(campo); — o valor gravado/retornado;
#Call:alias apiname.api.endpoint...(...); — a primeira linha da resposta do endpoint chamado;
a própria linha corrente de um #For (o nome usado na fonte do #For
vira, automaticamente, o alias de cada iteração).
Como referenciar um campo do alias:
:ALIAS.CAMPO
Referenciar um alias que ainda não foi produzido por nenhum comando anterior do script é tratado como
erro de script (HTTP 500) — nunca falha
silenciosamente.
Alias com mais de uma linha
Quando o mesmo alias é atribuído mais de uma vez (por exemplo, um INSERT:alias ... #RETURN(...)
repetido dentro de um #For), :ALIAS.CAMPO sempre reflete a
última atribuição. Apesar disso, todo o histórico de linhas fica guardado internamente
e pode ser usado de duas formas:
#For() :ALIAS do — repete um novo laço, uma vez por linha já acumulada (ver tópico
#For);
#AsInList( :ALIAS.CAMPO ) — monta a lista de todos os valores de um campo, pronta para
um IN (...) (ver tópico #AsInList / #QuotedStr).
Exemplo
#For()
:Embalagem do
#begin
INSERT:emb INTO EMBALAGEM_CORREIOS (CHAVE_EMPRESA, DESCRICAO)
VALUES (:Embalagem.CHAVE_EMPRESA, :Embalagem.DESCRICAO)
#RETURN(EMBALAGEM_CORREIO)
#end;
-- "emb" agora tem uma linha acumulada para cada registro inserido acima
#For() :emb do
#begin
select:itens PRODUCT from ITENS_PRODUCT_PV ppv
where ppv.EMBALAGEM = :emb.EMBALAGEM_CORREIO;
#end;
#SetVar ( )
Script Suportado: Somente Endpoint
Cria ou altera uma variável do script: um valor que pode mudar durante a execução,
como um contador, um total ou o número de falhas. A variável é usada como qualquer parâmetro, sempre
com : na frente (:nome).
#SetVar(:nome = valor);
Não existe declaração de tipo: o tipo vem do valor atribuído (inteiro, decimal ou texto). Também
não existe constante, porque um literal ou um parâmetro do endpoint já faz esse papel.
Ler o valor — sem #SetVar
O #SetVar serve só para criar ou alterar o valor. Depois que a variável
foi criada, para ler o valor basta escrever :nome, do mesmo jeito que um
parâmetro: no SQL, no #iif ( ) / #case ( ), no
#SetVarResult ( ), no #HttpParam, no início/fim do
#For ( ) e em qualquer lugar onde um parâmetro é aceito.
#SetVar(:erros = 0); -- cria (grava)
#SetVar(:erros = :erros + 1); -- altera (grava, lendo o valor atual)
#iif(:erros >= 3 then TRUE: { #Break; }, ELSE: { }); -- lê: só :erros, sem #SetVar
update IMPORTACAO set FALHAS = :erros where ID = :ID; -- lê no SQL
#SetVarResult(FALHAS = :erros); -- lê para devolver no Result
Escrever #SetVar(:erros) para ler o valor está errado: o #SetVar sempre
precisa de = valor.
Cada falha (erro ou timeout) soma 1 em :erros. O #iif lê o valor só com
:erros, sem #SetVar. Enquanto houver menos de 3 falhas, o
#SetResponseCode(200) substitui o 400 automático do bloco Error e a importação continua
na próxima página. Na terceira falha, o status vira 400 (ou 504 no timeout) e o #Break
encerra o laço. No fim, a resposta traz quantos produtos foram importados e quantas falhas aconteceram.
Exemplo — decidir pelo status do pedido
A loja virtual devolve o pedido com um campo status. Cada status leva a uma ação diferente,
e o #SetVar guarda o que foi decidido para usar depois, fora do #case.
#SetVar(:situacao = 'desconhecido');
#SetVar(:faturar = 'N');
#HttpNewUrl('https://api.exemplo.com.br/pedidos');
#HttpAddUrl('/' || :NUMERO);
#CallHTTP:PED(
TimeOut: 15 { #SetResponseCode(504); },
Error: { #SetVarResult(MOTIVO = #Http_Error(message)); },
Success: {
#case(:PED.status of
'A': {
#SetVar(:situacao = 'aberto');
#SetVar(:faturar = 'S');
},
'P': {
#SetVar(:situacao = 'pago');
#SetVar(:faturar = 'S');
update PEDIDO set PAGO = 'T' where NUMERO = :NUMERO;
},
'C': {
#SetVar(:situacao = 'cancelado');
update PEDIDO set CANCELADO = 'T' where NUMERO = :NUMERO;
},
ELSE: {
#SetVar(:situacao = 'status ' || :PED.status || ' sem tratamento');
}
);
}
);
#iif(:faturar = 'S' then
TRUE: { insert into FILA_FATURAMENTO (NUMERO) values (:NUMERO); },
ELSE: { }
);
#SetVarResult(SITUACAO = :situacao);
O #case escolhe o que fazer em cada status: A (aberto) e
P (pago) marcam o pedido para faturar, e o pago também atualiza a tabela;
C (cancelado) só marca o cancelamento; qualquer outro cai no ELSE. Depois,
um único #iif lê :faturar e coloca o pedido na fila de faturamento, sem
repetir o insert em cada status. A resposta devolve a :situacao decidida.
Select
Script Suportado: Endpoint e ObjectArray
Muito importante: apenas o selectsem alias alimenta a
resposta (Result) do endpoint. Um select:alias só fica disponível
internamente, para o resto do script consultar via :alias.campo — os
campos dele não aparecem automaticamente na resposta enviada ao cliente da API. Essa
regra é sem exceção — se os mesmos campos usados internamente também precisam ir na
resposta, o script declara dois selects: um com alias (uso interno) e outro sem alias
(resposta), mesmo que consultem os mesmos dados.
select — vira a resposta do endpoint
-- Pedidos da empresa, com filtros opcionais: cada bloco [ ] some
-- quando o parâmetro dele não é enviado na chamada.
select P.PEDIDO,
P.DATA_EMISSAO,
P.STATUS,
C.NOME as CLIENTE,
P.VALOR_TOTAL
from PEDIDOS P
join CLIENTES C on C.CLIENTE = P.CLIENTE
where P.EMPRESA = :EMPRESA
[ and P.STATUS = :STATUS ]
[ and P.DATA_EMISSAO >= :DATA_INICIAL ]
[ and P.DATA_EMISSAO <= :DATA_FINAL ]
order by P.DATA_EMISSAO desc;
O resultado desse comando é o JSON devolvido como resposta. Só os campos declarados na lista
Result do endpoint aparecem na resposta final — qualquer outro campo trazido pelo
select e não declarado lá é descartado.
select:alias — só fica em memória
-- 1) "cli" guarda os dados do cliente só para uso interno do script.
select:cli NOME, LIMITE_CREDITO
from CLIENTES
where CLIENTE = :CLIENTE;
-- 2) Esse select sem alias é a resposta: usa os campos de "cli"
-- para devolver o nome e o limite junto com os pedidos em aberto.
select P.PEDIDO,
P.DATA_EMISSAO,
P.VALOR_TOTAL,
:cli.NOME as NOME_CLIENTE,
:cli.LIMITE_CREDITO as LIMITE_CREDITO
from PEDIDOS P
where P.CLIENTE = :CLIENTE
and P.STATUS = 'ABERTO'
order by P.DATA_EMISSAO;
Se o select:cli não trouxer nenhuma linha, isso não é tratado como erro —
o restante do script continua normalmente, mas qualquer referência a :cli.CAMPO mais
adiante vai falhar (alias nunca produzido).
Paginação
O CrudSqlBuilder não tem um parâmetro específico para paginação de listas — mas ela
pode ser aplicada com os recursos que já existem. A paginação pode ser feita com parâmetros simples,
aceitos pelo próprio banco de dados, fazendo com que o banco retorne apenas os dados que se deseja
limitar na consulta: o select pede só as linhas da página, com a sintaxe de paginação do
banco do endpoint, em vez de trazer a tabela inteira. Quem diz qual trecho devolver são os
Parâmetros do endpoint, com o bloco condicional
[ ].
A sintaxe em cada banco
Banco
Linhas 201 a 300 (página 3, 100 por página)
Firebird
order by ID rows 201 to 300 — a contagem começa em 1. No Firebird 3 ou superior também vale offset 200 rows fetch next 100 rows only.
MySQL, PostgreSQL e SQLite
order by ID limit 100 offset 200 — o offset é quantas linhas pular.
SQL Server
order by ID offset 200 rows fetch next 100 rows only — exige order by.
Sempre com order by por uma chave única. Sem ordem definida, o banco
pode devolver as linhas em outra sequência a cada chamada — um registro aparece em duas páginas e
outro não aparece em nenhuma.
O SQL recebe só números prontos: quem calcula o trecho é quem chama, pelos parâmetros.
Paginando com os Parâmetros do endpoint
O jeito mais simples é deixar a página nas mãos de quem chama, usando as ferramentas que os
Parâmetros do endpoint já têm (ver
Parâmetros de entrada e [ ] Params Condicional):
Na aba de Parâmetros do endpoint, declare DE e ATE como
inteiros e opcionais (sem Required).
No SQL, coloque a cláusula de paginação do banco dentro de um bloco [ ]. Como o bloco
só entra no comando quando DE e ATE são enviados, o mesmo endpoint serve
para as duas coisas: com os parâmetros devolve só a página; sem eles, devolve tudo.
-- Firebird
select ID_CLIENTE, NOME, CIDADE
from CLIENTE
order by ID_CLIENTE
[ rows :DE to :ATE ]
Quem chama escolhe o trecho pela URL:
.../Listar_clientes?DE=1&ATE=100 -- clientes 1 a 100
.../Listar_clientes?DE=101&ATE=200 -- clientes 101 a 200
.../Listar_clientes -- todos (o bloco [ ] some)
Nos bancos que usam limit/offset, declare TAMANHO e
PULAR (quantas linhas pular) no lugar de DE/ATE:
-- MySQL, PostgreSQL e SQLite
select ID_CLIENTE, NOME, CIDADE
from CLIENTE
order by ID_CLIENTE
[ limit :TAMANHO offset :PULAR ]
-- SQL Server
select ID_CLIENTE, NOME, CIDADE
from CLIENTE
order by ID_CLIENTE
[ offset :PULAR rows fetch next :TAMANHO rows only ]
Paginação obrigatória: se o endpoint nunca deve devolver a tabela inteira, marque os
dois parâmetros como Required e escreva a cláusula sem os colchetes — sem eles a API
responde HTTP 400 antes de rodar o SQL.
Filtros junto com a paginação: os filtros opcionais continuam funcionando
normalmente, cada um no seu bloco [ and ... ] dentro do where; o bloco da
paginação fica por último, depois do order by.
Para quem chama saber quando parar, basta pedir o próximo trecho até ele vir vazio — ou oferecer um
segundo endpoint com o select count(*) da mesma consulta.
select ID_CLIENTE, NOME, CIDADE
from CLIENTE
where ATIVO = 'S'
[ and CIDADE = :CIDADE ]
order by ID_CLIENTE
[ rows :DE to :ATE ]
#StreamUrl ( )
Script Suportado: Endpoint e ObjectArray
Monta o endereço de um endpoint Stream (foto, PDF, XML...)
para ser devolvido dentro do JSON de outro endpoint. Uma lista, por exemplo, não carrega a imagem —
só o texto com o caminho dela, e quem consome (um <img src="...">, um aplicativo)
baixa cada arquivo sozinho. O #StreamUrl devolve o início desse
endereço como texto; o SQL completa com o nome do arquivo.
Endereço — sempre completo, formado pelo nome da API,
pela palavra fixa api e pelo caminho do endpoint. O endpoint precisa
existir, ser um GET e ter um Format do tipo
Stream.
Parênteses depois do endereço — sempre presentes. Levam, por
nome, os parâmetros da pasta base do endpoint (por exemplo
namecompany = 'acme'); ficam vazios, ( ), quando o endpoint não tem
parâmetro.
Valor — um texto fixo ('acme'), um parâmetro do endpoint
(:CHAVE_EMPRESA), o campo de um alias (:alias.campo) ou outra
macro. É um valor único por
requisição: com :alias.campo de várias linhas, vale a da última
linha. O valor de uma coluna da própria linha do SELECT não é
aceito.
thumb — opcional. Monta a URL da miniatura da imagem
(Miniaturas de imagem): a macro escreve thumb=true na URL.
Sem ele, escreve thumb=false (o original), porque o parâmetro thumb é
obrigatório nos GET de imagem. Só vale para endpoint de imagem (JPG, PNG,
WEBP); nos outros formatos não existe e a macro não o escreve.
O que a macro devolve é o endereço relativo, sem o endereço do servidor, com os
parâmetros da pasta, depois o thumb (imagens) e o $filename= sempre por último — para o SQL concatenar o nome do
arquivo logo depois:
A concatenação do nome do arquivo é do SQL, e o operador depende do banco: || no
Firebird, PostgreSQL e SQLite; + no SQL Server; CONCAT no MySQL.
Exemplo — com parâmetro da pasta base
Para um endpoint de fotos cuja pasta base tem um parâmetro (Produtos\:namecompany),
o valor pode ser fixo, vir de um parâmetro recebido ou do campo de um alias:
-- valor fixo
#StreamUrl(api_name.api.Catalogo.Produtos.Listar_fotos( namecompany = 'acme' ), thumb)
|| p.cod_produto || '_' || p.cod_cor || '_01.jpg' as foto
-- repassando o parâmetro enviado no GET
#StreamUrl(api_name.api.Catalogo.Produtos.Listar_fotos( namecompany = :chave_empresa ), thumb)
|| p.cod_produto || '_' || p.cod_cor || '_01.jpg' as foto
-- repassando o campo de um alias
#StreamUrl(api_name.api.Catalogo.Produtos.Listar_fotos( namecompany = :alias.chave_empresa ), thumb)
|| p.cod_produto || '_' || p.cod_cor || '_01.jpg' as foto
-- sem parâmetro (os parênteses continuam obrigatórios)
#StreamUrl(api_name.api.Catalogo.Produtos.Listar_fotos(), thumb)
|| p.cod_produto || '_' || p.cod_cor || '_01.jpg' as foto
Validações e cuidados
O endpoint informado precisa existir, ser GET e ter Format Stream; caso
contrário o endpoint que usa a macro responde HTTP 500 com o
erro de configuração.
Todo parâmetro obrigatório da pasta base precisa ser informado, e só podem ser
informados parâmetros que o endpoint declara — nome desconhecido ou obrigatório ausente é erro.
thumb em um endpoint que não é de imagem é erro.
Os valores são codificados para URL automaticamente, inclusive os que vêm de
:parâmetro ou de alias — aspas e caracteres especiais não quebram o SQL nem o
endereço.
O valor do parâmetro fica visível na URL devolvida no JSON. Quem tiver o
endereço pode alterá-lo; a proteção por empresa, quando necessária, vem da autenticação do
endpoint, não da URL.
Manipular Result
Macros que interferem diretamente no resultado devolvido pelo endpoint — preenchendo
um campo específico da resposta ou trocando o código HTTP retornado.
#SetVarResult ( )
Script Suportado: Endpoint e ObjectArray
Permite preencher manualmente o valor de um campo que será devolvido no resultado do
endpoint, direto no script — monta o JSON de resposta campo a campo, sem
precisar de um segundo select sem alias só pra nomear um valor já obtido por um
select:alias. Cada chamada define um campo da resposta e pode ser usado quantas vezes
forem necessárias: os campos vão se acumulando no mesmo objeto de resultado. Funciona em qualquer
lugar do script (dentro de #iif/#case, dentro dos blocos de
#CallNodeJS, etc.).
Foi desenvolvido principalmente pra escrever no Result do endpoint o conteúdo
devolvido por uma consulta externa — o caso mais comum sendo o retorno de uma
chamada #CallNodeJS. Via de regra, é o #SetVarResult que
você usa pra definir o valor que volta de uma chamada Node.js.
#SetVarResult(CAMPO = Valor);
Lê-se como uma atribuição: o campo do Result à esquerda do = recebe o
valor à direita. CAMPO é um identificador simples (sem :, sem aspas). Valor
passa pelo mesmo resolvedor de texto usado em qualquer SQL do script — tem o mesmo poder de
qualquer expressão da linguagem: literal, :param, :alias.campo, e até
#iif ( ) / #case ( ) embutido, quando o valor depende de uma condição.
Produz {"status":"Emitida","chave":"..."} — cada chamada acrescenta um campo.
Uso com #iif embutido — valor depende de uma condição
#SetVarResult(mensagem = #iif( :RESP.chaveAcesso=null then
TRUE:{ 'Nota fiscal aguardando processamento' },
ELSE:{ 'Nota fiscal emitida com sucesso' }
) );
O #iif aqui é o mesmo uso embutido descrito em
#iif ( ) / #case ( ) — o branch escolhido vira texto puro, que passa a
ser o próprio Valor do #SetVarResult.
O tipo do campo define o formato
O #SetVarResult só preenche o valor do campo, como uma atribuição a um
campo de dataset — ele não formata nada. Como o valor aparece no JSON depende do tipo
declarado do campo no Result:
INTEGER, BIGINT, DECIMAL e DOUBLE
PRECISION saem como número, sem aspas.
DATE sai como 2026-07-27 e DATE TIME como
2026-07-27T12:00:00 — sempre em ISO 8601.
STRING e TEXT saem como texto.
Vazio ou NULL em um campo numérico ou de data sai como null.
Um campo de data tem um único padrão: o ISO 8601. Um valor que não
seja uma data nesse padrão (27/07/2026, /Date(...)/) é rejeitado com
HTTP 500 — erro de configuração do endpoint —; para devolver uma
data em outro formato, declare o campo como STRING ou TEXT. Em um
campo numérico, um valor que não pode ser lido como número (por exemplo, texto em um campo
INTEGER) é devolvido como texto, sem erro. Isso vale também para os macros de
Datas em JSON: para devolver um timestamp Unix, declare o campo como
BIGINT e preencha com #SetDateTimeToUnix.
#SetVarResult(criado = #SetDateTimeToUnix(:pedido.data)); -- campo BIGINT -> "criado": 1785153600
Como escrever em um campo de um ObjectArray para ser enviado no Result
Hoje, Valor sempre vira string na resposta, mesmo quando o texto
resolvido é um array/objeto JSON válido — #SetVarResult(itens = ...) com um array
produziria "itens":"[{...}]" (um array inteiro escapado dentro de uma string), não um
array de verdade. A proposta é o #SetVarResult reconhecer quando Valor já é
um JSON válido (array ou objeto) e gravar como JSON de verdade nesse caso — assim, combinado com o
#SetFormatJson, um campo do Result declarado como
OBJECTARRAY passa a poder ser preenchido manualmente, com controle total de quais campos
aparecem:
Aqui :itens já precisa existir em memória como um alias multi-linha
(select:alias, #Call:alias, ou qualquer outro comando que registre um
alias com várias linhas). O #SetFormatJson projeta só os campos escolhidos
(produto, qtd) — descartando o resto; e esse #SetVarResult
grava o resultado como array de verdade no campo itens da resposta, desde que
itens esteja declarado como OBJECTARRAY no Result do
endpoint (ver ObjectArray).
#SetResponseCode ( )
Script Suportado: Somente Endpoint
Força o status HTTP final da resposta, por cima da decisão normal (200/201/204) —
opcional, e funciona em qualquer parte do script, não é exclusivo do
#CallNodeJS. O corpo da resposta continua sendo decidido normalmente
(select/#SetVarResult ( )) — só o código do status
muda.
#SetResponseCode(codigo);
codigo aceita um número literal ou uma referência (:param,
:alias.campo) — precisa resolver pra um HTTP status válido (100–599).
Por que existe — o caso do #CallNodeJS
Foi criado principalmente para uso dentro dos blocos do #CallNodeJS, por causa de uma
particularidade de desenho: o bloco TimeOut segue as regras normais de status
(200/201/204) por padrão — não é tratado como erro, porque o motor não sabe se a
chamada Node vai terminar mais tarde por conta própria (ver
#CallNodeJS ( )). Na prática, porém, muitas integrações são síncronas
de verdade — se estourou o tempo, a chamada falhou do ponto de vista de quem pediu, e a resposta
deveria refletir isso. É exatamente pra esse caso que #SetResponseCode existe: dentro
do bloco TimeOut, força o status que fizer sentido pro seu cenário (ex:
504 Gateway Timeout), em vez do 200 padrão.
O mesmo pode ser aplicado dentro do bloco Error de
#CallNodeJS ( ), #CallHTTP ( ) e
#CallDLL ( ), que por padrão já força
HTTP 400 automaticamente — chamando
#SetResponseCode ali dentro, o valor escolhido pelo programador
substitui o 400 automático.
⚠️Código ≥ 500 aciona o alerta de erro por e-mail automaticamente — o mesmo
mecanismo que avisa o administrador em qualquer falha real de servidor não distingue a origem: um
#SetResponseCode(500) ou superior, escolhido deliberadamente pelo programador,
dispara esse alerta exatamente como um erro de verdade dispararia (o exemplo acima, com
504, faz isso de propósito — um timeout na consulta de CEP é tratado como falha
grave o suficiente pra avisar o administrador). Se o cenário não for grave a esse ponto, prefira
um código 4xx, que não aciona o alerta.
Object Array
Macros que atuam sobre o ObjectArray declarado de um
endpoint — ligando o array a linhas do resultado principal, ou substituindo o SQL dele em tempo de
execução.
#JoinObjectArray { }
Script Suportado: Endpoint e ObjectArray
O ObjectArray roda como uma consulta separada
e totalmente independente do select principal — via de regra, o SQL do
ObjectArray roda primeiro. É o #JoinObjectArray{ } quem aplica a relação
entre os dois: uma regra do tipo master-detail que filtra a lista inteira já trazida
pelo ObjectArray, pra compor só o pedaço que corresponde a cada linha do select
principal, pelo campo de correlação indicado. Isso acontece só na hora de montar a resposta em
JSON — nunca dentro de nenhuma das duas consultas — e por isso o
#JoinObjectArray precisa ser declarado junto do select principal (ver
Sintaxe abaixo), nunca dentro do ObjectArray.
É esse desenho que torna o recurso rápido mesmo em volume alto. Exemplo: um endpoint que precisa
devolver produtos junto com as cores e os tamanhos cadastrados de cada um. Em vez de consultar cores
(ou tamanhos) uma vez para cada produto retornado — o que ficaria lento à medida que a
lista cresce — o motor busca a lista inteira de produtos numa consulta e a lista
inteira de cores em outra (a do ObjectArray), cada uma independente e
rodando uma única vez; só na montagem do JSON final é que cada cor é agrupada e anexada ao produto
correspondente, usando o campo de correlação declarado no #JoinObjectArray.
Sintaxe
select p.produto, p.cod_produto, p.descricao1 as descricao, p.custo
from produtos p where p.tipo_prod='MP'
#JoinObjectArray{ produto = :COR_PROD.produto }
#JoinObjectArray{ produto = :TAM_PROD.produto };
#JoinObjectArray{ campo_do_select = :ARRAY_NAME.campo_do_array } vai depois
do SQL de verdade, antes do ;. Lê-se como um join: o campo do select
principal (à esquerda, nome puro) é ligado ao campo do array (à direita, lido pelo alias
:ARRAY_NAME).
As chaves { } são obrigatórias, mesmo com uma única ligação. É sempre
#JoinObjectArray{ ... }; o que muda é só o que vai dentro. Uma vírgula separando mais de uma entrada dentro das MESMAS chaves é que decide se vira
chave composta ou cadeia (ver mais abaixo) — sem vírgula, é
uma ligação simples, de um campo só.
O exemplo acima tem duas ocorrências separadas, cada uma com suas próprias chaves —
são dois arrays independentes (COR_PROD e TAM_PROD), sem
relação um com o outro, cada um com seu próprio filtro. Isso é diferente de colocar os dois
dentro da MESMA chave: aí sim as condições passam a se relacionar (mesmo array = aplicadas
juntas; array diferente = uma dentro da outra).
campo_do_select — campo do lado deste select (ex:
produto em PRODUTOS), escrito como nome puro, à esquerda
do =.
:ARRAY_NAME.campo_do_array — o campo do lado do array (ex:
:COR_PROD.produto), à direita do =. ARRAY_NAME é o nome do
campo OBJECTARRAY declarado no
Result do endpoint, lido pelo alias (com :) como qualquer outro alias do
script. Ele continua com sua própria declaração de SQL e Parâmetros na aba
ObjectArray, exatamente como sempre — o macro só diz onde e como ligar, não
substitui a declaração.
Escrever #JoinObjectArray sem as chaves é um erro de configuração — o motor recusa a
configurar o endpoint e devolve HTTP 500 (erro de quem escreveu o
script, não de quem chamou a API) pedindo pra reescrever com { }.
Como funciona
A consulta declarada no ObjectArray roda uma única vez para a chamada inteira — nunca
uma vez por linha do select principal. Ela não recebe parâmetro nenhum vindo de cada linha, então
precisa se virar sozinha com o próprio filtro (no exemplo acima, repetindo
p.tipo_prod='MP' no join com produtos). As duas consultas —
a principal e a do ObjectArray — rodam de forma totalmente independente,
sem nenhuma relação de dependência entre elas.
É só depois, com as duas listas completas já em mãos, que o motor agrupa o resultado do
ObjectArray pelo campo de correlação e anexa cada grupo à linha correspondente do
select principal — tudo em memória, sem tocar o banco de novo. Nenhuma consulta é refeita por linha, e
o custo total continua sendo duas idas ao banco, não importa se o select principal
devolve 10 linhas ou 10 mil.
Se uma linha do select principal não tiver nenhuma correspondência no array, o campo aparece como
array vazio ([]) — nunca é omitido da resposta.
Sem o #JoinObjectArray, um campo OBJECTARRAY declarado no
Result continua funcionando como sempre: a consulta roda uma única vez e o resultado
inteiro é anexado a toda linha do select, sem nenhuma correlação — a opção certa
quando o array é o mesmo para todas as linhas. Use #JoinObjectArray quando o conteúdo
do array precisa mudar de acordo com cada linha.
Aninhado (array dentro de array)
O SQL declarado num ObjectArray pode ter, ele mesmo, um #JoinObjectArray no
final — correlacionando com outro OBJECTARRAY declarado dentro do próprio
Result desse array. Não tem limite de profundidade: cada nível pode ter o seu, e o
motor resolve tudo recursivamente antes de montar a resposta final.
-- Result do endpoint: PRODUTO (select principal) -> COR_MP (array) -> TAMANHO_MP (array dentro de COR_MP)
-- SQL do select principal:
select p.produto, p.cod_produto, p.descricao1 as descricao
from produtos p where p.tipo_prod='MP'
#JoinObjectArray{ produto = :COR_MP.produto };
-- SQL declarado no ObjectArray "COR_MP" (aba ObjectArray, não é um #JoinObjectArray comum):
select c.produto, c.cor, c.descricao_cor
from cores_mp c
#JoinObjectArray{ produto = :TAMANHO_MP.produto };
-- SQL declarado no ObjectArray "TAMANHO_MP" (dentro do Result de COR_MP):
select t.produto, t.tamanho, t.descricao_tamanho
from tamanhos_mp t;
Cada nível continua rodando como uma única consulta extra (não uma por linha) — o
correlacionamento do nível de baixo usa como base o resultado já filtrado do nível de
cima, então o custo continua sendo uma consulta por nível declarado, nunca por linha ou por
combinação.
A documentação HTML de cada endpoint (tabela de Result Fields e o exemplo de corpo da
resposta) expande automaticamente todos os níveis declarados, com indentação crescente a cada
OBJECTARRAY aninhado.
Mais de uma entrada nas mesmas chaves — chave composta e cadeia
Dentro de { } pode ter uma ou mais entradas campo_do_pai = :ARRAY_NAME.campo_do_array
separadas por vírgula. A regra de leitura é sempre a mesma, não importa o que vier: entrada com
o mesmo ARRAY_NAME da anterior junta mais um campo de correlação no mesmo nível; ARRAY_NAME diferente
abre um nível novo, aninhado dentro do array anterior.
Mesmo array — chave composta
select p.produto, p.cod_produto, p.descricao1 as descricao, p.unidade_uso
from produtos p
where p.tipo_prod = 'MP'
#JoinObjectArray{ produto = :COR_MP.produto, cor = :COR_MP.cor };
As duas entradas usam o mesmo array (COR_MP) — viram uma única consulta
extra, e uma linha de COR_MP só entra no grupo de uma linha do select principal se
todos os pares baterem juntos (produtoecor).
Equivalente a um filtro composto (AND), aplicado no mesmo nível.
Arrays diferentes — cadeia num único macro
select p.produto, p.cod_produto, p.descricao1 as descricao
from produtos p where p.tipo_prod='MP'
#JoinObjectArray{ produto = :COR_MP.produto, produto = :TAMANHO_MP.produto };
Aqui o segundo nome (TAMANHO_MP) é diferente do primeiro (COR_MP) — abre um
nível novo: primeiro filtra COR_MP por produto, depois, dentro
do resultado de COR_MP, filtra TAMANHO_MP por produto. Mesmo
resultado que declarar #JoinObjectArray{ produto = :TAMANHO_MP.produto }; direto no SQL do
ObjectArrayCOR_MP (seção anterior) — só que declarado tudo de uma vez, no
macro do select principal, sem precisar editar o SQL de cada array separadamente. As duas formas de
declarar aninhamento convivem: pode misturar composição (SQL do próprio array) com { } no
mesmo endpoint sem conflito.
É possível combinar os dois: repetir um nome pra chave composta num nível e depois trocar de nome pra
descer um nível, tudo na mesma lista — { m1 = :A.c1, m2 = :A.c2, m3 = :B.c3 } aplica
c1 e c2 juntos em A, e desce pra B dentro do
resultado de A.
#OverrideObjectArraySql { }
Script Suportado: Endpoint e ObjectArray
Substitui a consulta SQL declarada no OBJECTARRAY — ou seja, permite criar e
executar a consulta direto no endpoint, só dentro deste script, sem precisar
editar (ou duplicar) a declaração na aba ObjectArray.
Este mecanismo pode ser usado livremente: se você optar por não declarar a consulta
SQL dentro do ObjectArray, pode declará-la direto no script, via #OverrideObjectArraySql. Mas
quando a consulta é declarada completa direto no ObjectArray, isso favorece o
reaproveitamento do objeto — qualquer outro endpoint que referencie o mesmo
ARRAY_NAME já herda a consulta pronta, sem precisar reescrevê-la no próprio script. Use
#OverrideObjectArraySql pontualmente, quando UM endpoint específico precisa de um comportamento
diferente do padrão compartilhado.
Sintaxe
#OverrideObjectArraySql:COR_PROD{
select cor, cod_cor from cores
}
select p.produto, p.cod_produto, p.descricao1 as descricao
from produtos p where p.tipo_prod='MP'
#JoinObjectArray{ produto = :COR_PROD.produto };
#OverrideObjectArraySql:ARRAY_NAME{ ... } é um comando de topo, igual um select ou
INSERT. O :ARRAY_NAME logo depois do nome do macro indica qual array é
substituído (o mesmo padrão de #CreateTemporaryTable:ALIAS e de
select:alias), e o SQL fica entre as chaves { }. A ordem importa: precisa ser
declarado antes do select/#JoinObjectArray que referencia o
mesmo ARRAY_NAME, pra estar registrado na hora em que a correlação for resolvida.
Conceito: polimorfismo
O ObjectArray continua sendo a "interface" — a metadata (Result,
Parâmetros) usada pela documentação HTML e pelo exemplo de corpo da resposta continua
vindo sempre da declaração de sempre, nunca do script. #OverrideObjectArraySql
só troca a "implementação" concreta — a consulta que roda de verdade — pra esta chamada.
Escopo local: o override vale só dentro do script onde foi declarado. Nunca grava de
volta na declaração compartilhada — outro endpoint que referencie o mesmo ARRAY_NAME
continua vendo a consulta original, sem nenhum efeito colateral.
Colunas podem divergir do Result declarado — decisão deliberada (é polimorfismo, não
é preciso bater 1:1 com o que está documentado). O único requisito é que o(s) campo(s) de correlação
usado(s) pelo #JoinObjectArray existam no resultado do override; qualquer outro campo
fica por sua conta.
Documentação não muda: a doc HTML e o exemplo de body continuam mostrando a consulta
declarada na aba ObjectArray, nunca o override — quem lê a documentação pública não
fica sabendo que este endpoint específico usa uma consulta diferente.
Parâmetros: vêm do mesmo lugar de sempre (valores já resolvidos do endpoint), e por
estar declarado dentro do próprio script, também enxerga qualquer :alias.campo já
capturado por um comando anterior do mesmo script (select:alias,
INSERT:alias #RETURN, linha de um #For em andamento, etc.).
Validação
ARRAY_NAME precisa ser um campo OBJECTARRAY já declarado no
Result do endpoint — mesma checagem do #JoinObjectArray. Nome errado/
inexistente vira HTTP 500 (erro de quem escreveu o script).
INSERT / #RETURN ( )
Script Suportado: Endpoint e ObjectArray
#RETURN(campo) armazena, na lista do alias do INSERT, o
valor de um campo gerado pelo próprio banco no momento da gravação — tipicamente a
chave primária autoincremento. Sua necessidade depende de qual tabela
está sendo inserida, não é um recurso genericamente opcional: no insert da tabela
principal, ele é fundamental sempre que existir uma ou mais
Sub-Table dependente desse valor pra gravar a chave estrangeira, ou sempre que
o valor precisar ser reaproveitado depois em outro comando como :alias.campo.
Já no insert de uma Sub-Table em si, o #RETURN normalmente é
dispensável — cabe ao desenvolvedor identificar, entre as tabelas envolvidas no
endpoint, qual é a principal, e reservar o #RETURN pra ela (ver exemplo completo em
Post automático com Sub-Table, tópico Endpoint).
INSERT INTO CLIENTES (NOME, EMAIL) VALUES (:NOME, :EMAIL);
INSERT:cli INTO CLIENTES (NOME, EMAIL) VALUES (:NOME, :EMAIL)
#RETURN(CLIENTE);
Diferente do select:alias, o valor de #RETURNtambém
entra automaticamente na resposta do endpoint (respeitando a lista Result declarada) —
não é preciso um select adicional só para expor o dado recém-gravado.
Funciona da mesma forma nos bancos suportados (Firebird 3.0 ou superior, PostgreSQL, SQL Server, MySQL) — a sintaxe de
retorno de cada um é resolvida automaticamente pelo motor.
Retornando mais campos além da chave
Se a necessidade for expor apenas a chave primária gerada, basta usar
#RETURN(campo) como no exemplo acima, declarando esse campo no Result do
endpoint. Porém, se existir a necessidade de retornar outros campos como complemento —
não só a chave — pode ser aplicado um select logo depois, trazendo os campos desejados
(que também precisam estar declarados no Result):
INSERT:emb INTO EMBALAGEM_CORREIOS (
CHAVE_EMPRESA,
PESO_MAXIMO,
CALCULAR_DIMENSAO,
DESCRICAO
)
VALUES (
:CHAVE_EMPRESA,
:PESO_MAXIMO,
:CALCULAR_DIMENSAO,
:DESCRICAO
)
#RETURN(EMBALAGEM_CORREIO);
select EMBALAGEM_CORREIO, DESCRICAO from EMBALAGEM_CORREIOS
where EMBALAGEM_CORREIO = :emb.EMBALAGEM_CORREIO;
O select final busca a DESCRICAO do registro recém-inserido, usando o
EMBALAGEM_CORREIO acumulado no alias emb pelo #RETURN.
Obs.: se o INSERT ... #RETURN rodar dentro de um #For
(acumulando mais de uma linha no alias, ver tópico #For), o select de
complemento precisa trazer todas as linhas de uma vez — nesse caso, troque a
comparação direta por #AsInList():
where EMBALAGEM_CORREIO in (#AsInList( :emb.EMBALAGEM_CORREIO ));
#For ( )
Script Suportado: Endpoint e ObjectArray
Estrutura de repetição: #For() <fonte> do #begin ... #end; — a fonte é declarada,
fechada com do, e o corpo entre #begin e #end;
executa o loop dos itens da lista, com acesso aos campos do item corrente via :NOME.CAMPO
(o próprio nome da fonte vira o alias de cada iteração).
Existem três fontes possíveis, diferenciadas só pela sintaxe do que vem logo depois de #For():
Sintaxe
Fonte
Fonte vazia/ausente
#For() :NOME do
Array do corpo (body) da requisição, declarado como OBJECTARRAY
no endpoint.
HTTP 500
#For() select:alias sql do
Resultado de um SELECT
Laço não executa (não é erro)
#For() :alias do
Histórico já em memória — alias de script (select:alias,
INSERT:alias ... #RETURN, linha de outro #For).
HTTP 500
1) Array do body da requisição
#For()
:ITENS do
#begin
INSERT INTO PEDIDO_ITENS (PEDIDO, PRODUTO, QUANTIDADE)
VALUES (:PEDIDO, :ITENS.PRODUTO, :ITENS.QUANTIDADE);
#end;
ITENS precisa estar declarado como OBJECTARRAY no endpoint (ver tópico
ObjectArray) e existir como array no corpo (body) da requisição — assim que
a requisição chega, ele já é registrado internamente do mesmo jeito que um alias, então
:ITENS.CAMPO funciona mesmo antes do #For rodar. Se o array não existir no
body, o script é interrompido com HTTP 500.
2) Resultado de um SELECT
#For() select:prod PRODUTO, DESCRICAO from PRODUTOS where CATEGORIA = :CATEGORIA do
#begin
insert into RELATORIO_TEMP (PRODUTO, DESCRICAO) values (:prod.PRODUTO, :prod.DESCRICAO);
#end;
Se o SELECT não retornar nenhuma linha, não é erro — o laço simplesmente não executa nenhuma vez.
3) Histórico de um alias já em memória (:alias)
#For() :emb do
#begin
select:itens PRODUCT from ITENS_PRODUCT_PV ppv
where ppv.EMBALAGEM = :emb.EMBALAGEM_CORREIO;
#end;
Itera cada linha que já foi atribuída ao alias emb em algum comando anterior do script
(ver tópico Alias). Referenciar um alias que nunca foi produzido é erro de script
(HTTP 500), mesma consequência de um array do body ausente.
Resultado gerado dentro do laço
Um select sem alias ou um INSERT ... #RETURN(...) executado dentro do corpo
do #For não sobrescreve a resposta a cada volta — os resultados de todas as iterações
são acumulados em uma única lista na resposta final.
#For dentro de outro #For
É permitido aninhar — um #For dentro do corpo de outro. Cada nível tem seu próprio
alias/nome de fonte, então não há conflito entre o alias do laço externo e do interno:
#For() :PEDIDOS do
#begin
#For() select:itens ITEM, QUANTIDADE from PEDIDO_ITENS where PEDIDO = :PEDIDOS.PEDIDO do
#begin
update ESTOQUE set QUANTIDADE = QUANTIDADE - :itens.QUANTIDADE where PRODUTO = :itens.ITEM;
#end;
#end;
4) Contador — #For() :i = inicio to fim do
Repete o corpo um número fixo de vezes, sem precisar de nenhuma fonte de dados. A variável do laço
segue a mesma regra de qualquer alias — sempre com : — e dentro do corpo é lida como
:i. O uso mais comum é a paginação de uma API externa
(page=1, page=2, ...) com o #CallHTTP.
#For() :i = 1 to 10 do
#begin
insert into LOG_TESTE (NUMERO) values (:i);
#end;
Regra
Comportamento
Início e fim
Número inteiro escrito direto, :param ou :alias.campo. São lidos
uma única vez, na entrada do laço — mudar o valor dentro do corpo não altera
o número de voltas.
Fim menor que o início
Laço não executa (não é erro)
Valor que não é número inteiro
HTTP 500
Limite de segurança
No máximo 10.000 voltas por laço; acima disso o script é interrompido com
HTTP 500 — protege a conexão de um parâmetro enviado errado.
Cancelamento
Pode ser cancelado pelo Task Monitor, como o #IntervalMS.
Pode ser aninhado como qualquer #For, inclusive misturado com as outras fontes.
Cada nível tem a sua variável; repetir no laço de dentro o nome do laço de fora é erro de script
(HTTP 500).
#For() :i = 1 to 10 do
#begin
#For() :x = 1 to 3 do
#begin
insert into GRADE (LINHA, COLUNA) values (:i, :x);
#end;
#end;
#Break — sair do laço antes do fim
#Break; encerra o #For em que está (só o mais interno, quando há
aninhamento) e o script continua no comando seguinte ao #end;. Vale para as quatro
fontes do #For. Normalmente fica dentro de um #iif.
A variável criada com #SetVar ( ) mantém o valor entre as voltas do
#For: aqui ela soma o campo valor de cada item do array
item do body e sai do laço quando o total passa de 1000.
Exemplo — paginação com o total de páginas num resumo
-- Cada página da API vem assim:
-- {"pedidos":[ ... ], "resumo":{"pagina":1, "total_paginas":8}}
#SetVar(:total_paginas = 0);
-- 1) A primeira chamada já traz a página 1 dos pedidos e o resumo.
#HttpNewUrl('https://api.exemplo.com.br/pedidos');
#HttpParam('page', 1);
#HttpParam('limit', 100);
#CallHTTP:PAG1(
TimeOut: 15 { #SetResponseCode(504); },
Error: { #SetVarResult(MOTIVO = #Http_Error(message)); },
Success: {
-- grava os pedidos da página 1
#SetImportJsonArray(:pedidos, :PAG1.pedidos, null);
#For() :pedidos do
#begin
insert into PEDIDO_IMPORTADO (NUMERO, CLIENTE, VALOR_TOTAL)
values (:pedidos.numero, :pedidos.cliente, :pedidos.valor_total);
#end;
-- o total de páginas está dentro do objeto "resumo"
#SetImportJsonArray(:resumo, #SetFormatJson(:PAG1), 'resumo');
#SetVar(:total_paginas = :resumo.total_paginas);
}
);
-- 2) As demais páginas, da 2 até o total informado no resumo.
#For() :pag = 2 to :total_paginas do
#begin
#HttpNewUrl('https://api.exemplo.com.br/pedidos');
#HttpParam('page', :pag);
#HttpParam('limit', 100);
#CallHTTP:PAGINA(
TimeOut: 15 { #Break; },
Error: {
#SetVarResult(MOTIVO = #Http_Error(message));
#SetResponseCode(200);
#Break;
},
Success: {
#SetImportJsonArray(:pedidos, :PAGINA.pedidos, null);
#For() :pedidos do
#begin
insert into PEDIDO_IMPORTADO (NUMERO, CLIENTE, VALOR_TOTAL)
values (:pedidos.numero, :pedidos.cliente, :pedidos.valor_total);
#end;
}
);
#IntervalMS(200);
#end;
A primeira chamada já grava os pedidos da página 1. Como o total de páginas está dentro do objeto
resumo, o #SetImportJsonArray transforma esse objeto num alias de uma linha
e o #SetVar guarda o total. A variável começa em 0: se a primeira chamada falhar, o
laço (de 2 até 0) não executa, e o script devolve o status e o MOTIVO da falha.
Exemplo — paginação por cursor (próxima página)
-- Cada página da API vem assim (comum em plataformas de e-commerce):
-- {"pedidos":[ ... ], "proxima_pagina":"eyJpZCI6MTAwfQ"}
-- Na última página, "proxima_pagina" vem null.
#SetVar(:cursor = '');
-- 1) A primeira chamada, sem cursor, traz a página 1 e o cursor da próxima.
#HttpNewUrl('https://api.exemplo.com.br/pedidos');
#HttpParam('limit', 100);
#CallHTTP:PAG1(
TimeOut: 15 { #SetResponseCode(504); },
Error: { #SetVarResult(MOTIVO = #Http_Error(message)); },
Success: {
#SetImportJsonArray(:pedidos, :PAG1.pedidos, null);
#For() :pedidos do
#begin
insert into PEDIDO_IMPORTADO (NUMERO, CLIENTE, VALOR_TOTAL)
values (:pedidos.numero, :pedidos.cliente, :pedidos.valor_total);
#end;
#SetVar(:cursor = :PAG1.proxima_pagina);
}
);
-- 2) Segue o cursor até ele vir vazio. O 1000 é só um limite de segurança.
#For() :i = 1 to 1000 do
#begin
#iif(:cursor=null then TRUE: { #Break; }, ELSE: { });
#HttpNewUrl('https://api.exemplo.com.br/pedidos');
#HttpParam('limit', 100);
#HttpParam('cursor', :cursor);
#CallHTTP:PAGINA(
TimeOut: 15 { #Break; },
Error: {
#SetVarResult(MOTIVO = #Http_Error(message));
#SetResponseCode(200);
#Break;
},
Success: {
#SetImportJsonArray(:pedidos, :PAGINA.pedidos, null);
#For() :pedidos do
#begin
insert into PEDIDO_IMPORTADO (NUMERO, CLIENTE, VALOR_TOTAL)
values (:pedidos.numero, :pedidos.cliente, :pedidos.valor_total);
#end;
#SetVar(:cursor = :PAGINA.proxima_pagina);
}
);
#IntervalMS(200);
#end;
Aqui a API não informa quantas páginas existem: cada resposta traz o cursor da próxima, e na última
ele vem vazio. O #For usa um limite alto só por segurança, e o #iif com
=null encerra o laço com #Break quando não há mais páginas (ou quando a
primeira chamada falhou e o cursor continuou vazio).
Exemplo — paginação por header (há mais registros?)
-- Esta API manda a paginação nos headers da resposta:
-- X-Tem-Mais-Registros: 1 (ainda existem registros a buscar) ou 0 (acabou)
-- O corpo traz a lista: {"pedidos":[ {"numero": 10, "cliente": "...", "alterado_em": "2026-10-28 09:15:00"}, ... ]}
-- A próxima chamada pede os registros alterados depois do último recebido.
#SetVar(:alterado_apos = '2026-01-01 00:00:00');
-- O 1000 é só um limite de segurança.
#For() :i = 1 to 1000 do
#begin
#HttpNewUrl('https://api.exemplo.com.br/pedidos');
#HttpParam('alterado_apos', :alterado_apos);
#CallHTTP:LOTE(
TimeOut: 15 { #Break; },
Error: {
#SetVarResult(MOTIVO = #Http_Error(message));
#SetResponseCode(200);
#Break;
},
Success: {
#SetImportJsonArray(:pedidos, :LOTE.pedidos, null);
#For() :pedidos do
#begin
insert into PEDIDO_IMPORTADO (NUMERO, CLIENTE, ALTERADO_EM)
values (:pedidos.numero, :pedidos.cliente, :pedidos.alterado_em);
#SetVar(:alterado_apos = :pedidos.alterado_em);
#end;
}
);
-- o header avisa se ainda tem mais registros; se não tem, encerra
#iif(#HttpReceivedHeader(:LOTE, 'X-Tem-Mais-Registros') = '1' then TRUE: { }, ELSE: { #Break; });
#IntervalMS(200);
#end;
Aqui a paginação não está no corpo: o header X-Tem-Mais-Registros diz se ainda há o que
buscar, e cada chamada pede os registros alterados depois do último recebido (alterado_apos).
O #HttpReceivedHeader lê o header da última chamada do alias LOTE; quando ele vem
0, ou não vem, o #iif encerra o laço com #Break.
O #Break dentro do TimeOut/Error encerra a paginação na primeira
falha, sem continuar chamando as páginas seguintes; o #IntervalMS espaça as chamadas para
não estourar o limite de requisições da API (HTTP 429).
Lembre que o bloco Error sempre devolve status 400, mesmo com o #Break. Para
devolver outro status, use #SetResponseCode no próprio bloco, como no exemplo (200, para
entregar o que já foi importado e o MOTIVO da falha).
#CreateTemporaryTable ( ) / #TemporaryTable
Script Suportado: Endpoint e ObjectArray
Cria uma tabela temporária por requisição, existe só durante a transaction do script e
some sozinha ao final — útil pra armazenar resultado intermediário (ex: staging de um cálculo em várias
etapas) sem precisar de uma tabela permanente no banco.
Não existe sintaxe SQL nova aqui. Todo comando que usa a tabela temporária —
INSERT, SELECT, UPDATE, com ou sem VALUES, com ou
sem SELECT como fonte — é SQL padrão, exatamente igual ao que já se
escreveria contra uma tabela normal do banco. #TemporaryTable.ALIAS é só uma
referência interna do motor de script: antes do comando rodar, essa referência é
substituída pelo nome real da tabela física gerada pra essa requisição — o banco
nunca vê #TemporaryTable, só o nome de tabela de verdade.
Sintaxe
#CreateTemporaryTable:ALIAS(
PRODUTO VARCHAR(20),
QUANTIDADE INTEGER,
PRECO DECIMAL(15,2)
)
#createindex (PRODUTO);
INSERT INTO #TemporaryTable.ALIAS (PRODUTO, QUANTIDADE, PRECO) VALUES (:PRODUTO, :QUANTIDADE, :PRECO);
select PRODUTO, QUANTIDADE, PRECO from #TemporaryTable.ALIAS where QUANTIDADE > 0;
ALIAS — nome lógico usado depois em qualquer select/INSERT/
UPDATE do mesmo script via #TemporaryTable.ALIAS. Não é o nome físico real da
tabela no banco (esse é gerado automaticamente, único por requisição).
Colunas entre parênteses — DDL igual ao de um CREATE TABLE normal, direto no dialeto do
banco de destino do endpoint.
#createindex (COLUNA) — opcional, zero ou mais, cada um sem ;
próprio. Todo o bloco (#CreateTemporaryTable + colunas + índices) fecha num único
; no final. O nome do índice não é declarado: é gerado
automaticamente. Para um índice composto, liste as colunas separadas por vírgula:
#createindex (COR, TAMANHO).
Tipos de coluna
O motor não traduz nem valida os tipos: o texto entre parênteses vai direto
para o banco do endpoint, como um CREATE TABLE comum — então valem os tipos do
dialeto desse banco, junto com qualquer restrição dele (NOT NULL,
DEFAULT). Os bancos diferem bastante em alguns tipos:
Dado
Firebird
PostgreSQL
MySQL
SQL Server
Igual nos 4 bancos
Inteiro
INTEGER
INTEGER
INTEGER
INTEGER
Sim
Inteiro pequeno
SMALLINT
SMALLINT
SMALLINT
SMALLINT
Sim
Inteiro grande
BIGINT
BIGINT
BIGINT
BIGINT
Sim
Decimal exato
DECIMAL(15,2)
DECIMAL(15,2)
DECIMAL(15,2)
DECIMAL(15,2)
Sim
Ponto flutuante
DOUBLE PRECISION
DOUBLE PRECISION
DOUBLE PRECISION
DOUBLE PRECISION
Sim
Texto curto
VARCHAR(20)
VARCHAR(20)
VARCHAR(20)
VARCHAR(20)
Sim
Data
DATE
DATE
DATE
DATE
Sim
Hora
TIME
TIME
TIME
TIME
Sim
Texto longo
BLOB SUB_TYPE TEXT
TEXT
TEXT
VARCHAR(MAX)
Não
Data e hora
TIMESTAMP
TIMESTAMP
DATETIME
DATETIME2
Não
Booleano
BOOLEAN
BOOLEAN
BOOLEAN
BIT
Não
As linhas marcadas Sim são o subconjunto portável — o mesmo texto
funciona em qualquer um dos quatro bancos, e é o que o autocompletar do editor sugere. Os demais
(texto longo, data e hora, booleano) dependem do banco: use o da coluna do seu banco. Cuidado com o
TIMESTAMP do SQL Server, que não é uma data — é um
contador de versão de linha; lá, use DATETIME2.
Como o tipo declarado é usado. Quando um alias vem de uma tabela temporária, o
#SetFormatJson usa o tipo declarado aqui para montar o JSON:
INTEGER, SMALLINT, BIGINT, FLOAT,
DOUBLE PRECISION, NUMERIC e DECIMAL saem como número;
DATE, TIMESTAMP e DATETIME saem como data
(ISO 8601); qualquer outro tipo sai como texto. O tipo precisa ser o primeiro termo
depois do nome da coluna (quantidade integer) — uma restrição logo depois do tipo
(integer not null) faz a coluna sair como texto.
Índice com mais de um campo
Para criar um único índice sobre dois ou mais campos, liste todos eles entre os
parênteses, separados por vírgula. É o que ajuda quando o script filtra ou junta a tabela temporária
usando esses campos juntos (por exemplo, produto + cor + tamanho):
No exemplo acima são criados dois índices: um só sobre PRODUTO e outro
composto por COR e TAMANHO. A ordem dos campos dentro dos
parênteses importa — o índice serve melhor para consultas que usam o primeiro campo da lista (e, em
seguida, o segundo). Cada #createindex cria um índice, então use um por combinação de
campos que você realmente consulta.
Populando via SELECT (INSERT ... SELECT)
#TemporaryTable.ALIAS é só uma máscara de texto, trocada pelo nome físico real
da tabela antes de qualquer outra coisa acontecer com o comando — não importa a forma do SQL. Funciona
igual num INSERT ... VALUES (...), num INSERT ... SELECT, num
UPDATE ou num select puro.
INSERT INTO #TemporaryTable.TBL (
PF_PJ,
CLIENTE,
COD_CLIENTE
)
SELECT
C.PF_PJ,
C.CLIENTE,
C.COD_CLIENTE
FROM clientes C;
O SQL que chega de verdade no banco (depois da máscara resolvida pro nome físico real, ex:
TMP_a1b2c3d4e5f6_1) é um INSERT ... SELECT padrão, sem nenhuma sintaxe
especial do motor — o mesmo texto testaria igual direto num cliente SQL comum (ex: IBExpert), trocando
só #TemporaryTable.TBL pelo nome físico da vez.
Várias tabelas temporárias na mesma chamada
Não tem limite de quantas #CreateTemporaryTable um script pode declarar — cada uma com seu
próprio ALIAS, independente das outras. Um uso comum é montar duas (ou mais) listas
separadas e combiná-las no final, por exemplo com UNION ALL:
#CreateTemporaryTable:TBL(
PF_PJ VARCHAR(2),
CLIENTE INTEGER,
COD_CLIENTE VARCHAR(12)
)
#createindex (cliente);
insert into #TemporaryTable.TBL (pf_pj, cliente, cod_cliente) VALUES ('PF', 1, '01');
insert into #TemporaryTable.TBL (pf_pj, cliente, cod_cliente) VALUES ('PJ', 2, '02');
#CreateTemporaryTable:TBL_2(
PF_PJ VARCHAR(2),
CLIENTE INTEGER,
COD_CLIENTE VARCHAR(12)
)
#createindex (cliente);
insert into #TemporaryTable.TBL_2 (pf_pj, cliente, cod_cliente) VALUES ('PF', 1, '99');
insert into #TemporaryTable.TBL_2 (pf_pj, cliente, cod_cliente) VALUES ('PJ', 2, '98');
select C.PF_PJ, C.CLIENTE as CLIENTE, C.COD_CLIENTE
from #TemporaryTable.TBL as c
union all
select C.PF_PJ, C.CLIENTE as CLIENTE, C.COD_CLIENTE
from #TemporaryTable.TBL_2 as c;
Repare que os dois #createindex (cliente) não colidem entre si — o nome de cada índice é
gerado automaticamente a partir da tabela física correspondente, então
ALIAS diferentes nunca disputam o mesmo nome de índice.
O select final (o que vira o Result do endpoint) lê das duas tabelas
temporárias via UNION ALL — o tipo de cada campo do resultado é detectado automaticamente
a partir do que a consulta de teste devolve:
Field Name
Field Type
Size
CLIENTE
INTEGER
COD_CLIENTE
STRING
10
PF_PJ
STRING
2
Comportamento por banco
Banco
Como é criada
Como some ao final
PostgreSQL
CREATE TEMP TABLE ... ON COMMIT DROP (nativo)
Automático, pelo próprio banco
Firebird / SQL Server / MySQL
Tabela real, com nome físico único gerado por requisição
DROP TABLE explícito, rodado antes do commit final da transaction
Firebird — logo após o CREATE TABLE (e cada #createindex), o
motor executa um COMMIT RETAINING automaticamente, senão o próximo comando do script
não reconhece a tabela recém-criada ("Table unknown"). Efeito colateral: qualquer trabalho feito
antes desse ponto no mesmo script já fica gravado de forma permanente — um erro
posterior no script só desfaz (rollback) o que vier depois do
#CreateTemporaryTable, não antes.
MySQL — CREATE TABLE/DROP TABLE fazem commit implícito lá
(não são transacionais nesse banco); mesmo efeito colateral do Firebird se aplica.
#Call ( )
Script Suportado: Endpoint e ObjectArray
Executa outro endpoint internamente, dentro do mesmo processo — da própria API ou
de outra API disponível no mesmo servidor. O endereço é sempre completo, no formato
<nomedaapi>.api.<caminho>: primeiro o nome da API, depois a palavra fixa
api, depois o caminho do endpoint chamado. Não existe forma implícita — mesmo chamando
um endpoint da própria API, o nome dela vem sempre na frente. É por ser a única macro que resolve a
chamada em tempo de execução (as demais já nascem fixas na declaração do endpoint) que o endereço
precisa ser explícito, sempre.
Os parâmetros entre parênteses podem ser passados por posição — o primeiro valor
vai para o primeiro parâmetro declarado no endpoint chamado, o segundo para o segundo, e assim por
diante — ou por nome (nome = valor, ver seção abaixo), que evita
depender da ordem exata em que o endpoint chamado declarou os parâmetros dele.
#Call crudapi.api.ESTOQUE.BAIXAR.POST(produto = :PRODUTO, quantidade = :QUANTIDADE);
#Call:res crudapi.api.FRETE.CALCULAR(cep_origem = :CEP_ORIGEM, cep_destino = :CEP_DESTINO, peso = :PESO);
-- A partir do momento que o #Call recebe um alias (":res"), TODOS os campos
-- do result do endpoint chamado passam a compor esse alias. Supondo que
-- FRETE.CALCULAR devolva os campos VALOR e PRAZO, eles já ficam
-- disponíveis no restante do script assim:
-- :res.VALOR, :res.PRAZO
Com alias (#Call:alias), todas as linhas da resposta do endpoint
chamado ficam disponíveis via :alias.campo (o valor singular reflete a última linha) —
pelo mesmo mecanismo de qualquer outro alias, inclusive o histórico completo, utilizável depois com
#For() :alias do ou #AsInList(...). Sem alias, é uma chamada "dispare e
esqueça": nada fica guardado em memória.
Se o endpoint chamado devolver um erro, o script inteiro é interrompido com
HTTP 500, deixando claro qual endpoint falhou.
Iterando o resultado de um #Call
#Call:correio crudapi.api.pedidos.pedido_integrado.listar_embalagens(chave = :chave_empresa);
#For() :correio do
#begin
update EMBALAGEM_CORREIOS set
PESO_MAXIMO = :correio.PESO_MAXIMO,
DESCRICAO = :correio.DESCRICAO
where CHAVE_EMPRESA = :correio.CHAVE_EMPRESA;
#end;
Como o #Call:alias guarda o histórico de todas as linhas retornadas, o resultado dele
pode ser percorrido depois com um #For() :alias do normal (ver tópico
#For), aplicando um comando por linha devolvida pelo endpoint chamado.
Própria API ou outra API — mesmo formato
Não existe marcador especial pra "outra API" — é sempre o mesmo endereço completo, muda só qual
nome vem antes do .api.:
-- Endpoint da própria API
#Call:res crudapi.api.pedidos.pedido_integrado.listar_embalagens(chave = :chave_empresa);
-- Endpoint de outra API cadastrada no mesmo servidor
#Call:res outraapi.api.pedidos.pedido_integrado.listar_embalagens(chave = :chave_empresa);
O restante do comando (alias, parâmetros por posição ou por nome, acumulação de linhas) funciona
exatamente igual, seja a chamada dentro da própria API ou de outra.
Corpo JSON — em vez de parâmetros posicionais
Quando o payload é mais rico (objetos aninhados, listas) e digitar campo a campo seria repetitivo,
o #Call aceita um corpo JSON completo no lugar dos parâmetros posicionais — o único
argumento passa a ser um bloco { }, em vez de uma lista separada por vírgula:
O restante do comportamento é o mesmo: com alias, o resultado fica disponível via
:alias.campo; sem alias, é "dispare e esqueça".
Parâmetros por nome, sem bloco { }
Pra um punhado de parâmetros simples, escalares, não é preciso o bloco { } completo —
dá pra escrever nome = valor direto dentro dos mesmos parênteses da chamada posicional,
separando por vírgula. O endereçamento passa a ser por nome (bate com o nome do
parâmetro declarado no endpoint chamado), não mais por posição:
Todo argumento precisa seguir esse formato (nenhum posicional misturado) — se qualquer um deles não
começar com um nome de campo seguido de = (ou :), o #Call
inteiro volta a ser interpretado como posicional.
#CallNodeJS ( )
Script Suportado: Somente Endpoint
Invoca uma função de código escrita em Node.js, local ao servidor — pra cobrir os
casos que fogem do que SQL/PL-SQL resolve bem, como aplicar uma transformação mais livre ou
usar uma biblioteca Node. Para chamar uma API externa por HTTP, sem Node.js, existe
o #CallHTTP ( ), com a mesma estrutura de TimeOut,
Error e Success. Veja Node.js (integração) pra
instalação e configuração.
Module é sempre "arquivo + função exportada" — nunca um verbo HTTP. A comunicação com o
Node não é uma requisição de rede, é uma invocação direta de função dentro de um processo local.
Bloqueia até a função Node devolver um resultado, com um limite de tempo (TimeOut, em
segundos).
⚠️Alias e os três blocos são obrigatórios —
#CallNodeJS:alias(...) sempre precisa do alias e das três declarações,
TimeOut, Error e Success. Não são "tratamento de erro"
opcional: são a forma do endpoint decidir explicitamente o que fazer em cada um dos três
desfechos possíveis da chamada — faltar qualquer um deles (alias incluso) é erro de configuração
do endpoint.
Success — a função Node devolveu dentro do tempo, sem lançar exceção. O
status HTTP segue as regras normais do endpoint (200/201/204), de acordo com o que o bloco
decide gravar/devolver — via de regra, com #SetVarResult ( ):
é justamente pra esse cenário que ele foi pensado, escrever no Result do
endpoint o conteúdo que voltou de uma consulta externa (o retorno do Node, nesse caso).
TimeOut — o tempo limite passou sem resposta. Não é erro por padrão: a chamada
pode muito bem terminar depois, só não dentro dessa requisição (ex: emissão de nota fiscal no
SEFAZ, que pode legitimamente demorar mais que o timeout configurado sem que isso signifique
falha). O status HTTP também segue as regras normais, sem nada forçado — na prática,
HTTP 200 por padrão, a menos que o próprio bloco chame
#SetResponseCode ( ), pra integrações verdadeiramente
síncronas onde estourar o tempo é, sim, uma falha do ponto de vista de quem chamou.
Error — a função Node lançou uma exceção (ex: credencial inválida, certificado
vencido, rejeição de negócio do lado de fora). A mensagem da exceção fica disponível através de
#Node_Error(message) dentro do bloco — não é um alias declarado (como
:RESP), é um resultado que o próprio motor captura durante a chamada, por isso usa
sintaxe de comando (#...) em vez do :alias.campo, reservado pra dados
de entrada. Diferente dos outros dois, esse bloco força o status HTTP para 400
automaticamente — o corpo da resposta continua livre, escolhido por
#SetVarResult, só o código do status é fixo por padrão (chamar
#SetResponseCode ( ) dentro do bloco substitui esse
400 automático por outro código, escolhido pelo desenvolvedor).
HTTP 400 fica só no log, sem avisar o administrador da API —
cada desenvolvedor decide o nível de risco do próprio caso. Já ≥
500 aciona o alerta por e-mail quase imediatamente (ver
#SetResponseCode ( )). Por isso, usar
#SetResponseCode pra forçar ≥ 500 dentro de
Error/TimeOut funciona como um jeito de monitorar a saúde do processo
externo: vira alerta ativo pro administrador, em vez de ficar só registrado no log.
A única situação em que o motor força um erro 500 automático, independente dos
blocos declarados, é a invocação em si não conseguir acontecer — módulo/função Node não
encontrado, ou integração Node.js não configurada no servidor. Isso não é um resultado de negócio
pra ser tratado no script, é um erro de configuração do endpoint.
💡O retorno do Node fica disponível em tempo de execução através do
alias declarado no comando — :RESP no exemplo a seguir. Mas isso é
só uma das formas de trazer o dado de volta: em vez de devolver o payload inteiro pelo
return, o módulo Node pode gravar o resultado chamando um Endpoint
específico (uma requisição HTTP comum, autenticada pelo token efêmero — ver
Node.js (integração)) assim que captura a informação, e devolver só
um Success vazio (sem payload) ao terminar. Nesse caso, dentro de Success
(ou Error — o mesmo padrão serve pros dois) o script faz um select:alias
comum pra ler o que foi gravado:
select:nfe idChave, dataEmissao from NOTAS where NOTA = :ID_NOTA;
Isso funciona porque TimeOut/Error/Success são blocos de
script normais — qualquer comando (select, update, #Call
etc.) já é suportado ali, não é um recurso novo. Repare que essa gravação acontece numa
transaction separada da do script (é outra requisição, com sua própria
conexão) — funciona sem problema porque o #CallNodeJS só devolve o controle pro
script depois que a função Node termina, e nesse ponto o endpoint chamado já
respondeu (e já fez o próprio commit).
#CallNodeJS:RESP(
Module: "emitirSefaz",
Params: { idNota = :ID_NOTA, valor = :VALOR_TOTAL },
TimeOut: 15 {
update nota set status = 55 where nota = :ID_NOTA;
#SetVarResult(status = 'Aguardando Sefaz');
},
Error: {
update nota set status = 618 where nota = :ID_NOTA;
#SetVarResult(status = 'Nota fiscal rejeitada');
#SetVarResult(motivo = #Node_Error(message));
#SetResponseCode(504);
},
Success: {
update nota set status = 60, chave_acesso = :RESP.chaveAcesso where nota = :ID_NOTA;
#SetVarResult(status = 'Emitida');
#SetVarResult(chave = :RESP.chaveAcesso);
}
);
Repare que os três blocos gravam e devolvem de um jeito diferente cada um, mas só o
Error tem uma consequência automática (status 400) — Success e
TimeOut seguem as regras normais do endpoint, o resto de cada bloco é escolha livre
do desenvolvedor. Nesse exemplo (emissão de nota fiscal, genuinamente assíncrona) faz sentido
não forçar nenhum status em TimeOut — a nota pode ser emitida
depois. Já numa integração síncrona (uma consulta que só faz sentido responder na hora, como
buscar um endereço por CEP), o mesmo TimeOut normalmente deveria usar
#SetResponseCode ( ) pra refletir a falha no status HTTP —
ver exemplo completo em Node.js (integração).
⚠️O retorno do módulo é sempre JSON — o que a função devolve
(return) precisa ser serializável em JSON: um objeto (vira uma linha em
:alias.campo), um array de objetos (uma linha por item, pronto para
#For) ou um valor simples (texto, número, boolean — fica em
:alias.VALUE). Sem return (ou null), o Success
roda normalmente com o alias vazio. Nunca devolva uma função, uma Promise não resolvida, um
Buffer ou um objeto com referência circular. Ver Contrato obrigatório do
módulo em Node.js (integração).
Atenção de tipagem: os campos de um alias já registrado (usados dentro de
Params) chegam do lado Node sempre como string, mesmo quando o valor é
numérico — é preciso converter (Number(...)) antes de usar como número dentro do módulo.
#CallDLL ( )
Script Suportado: Somente Endpoint
Invoca uma função de uma DLL nativa (Windows, 64 bits), local ao servidor — pra
aproveitar bibliotecas prontas que não existem em SQL nem em Node.js, como componentes fiscais
(NF-e, NFS-e, CT-e), TEF, boleto ou o SDK de um equipamento. A estrutura é a mesma do
#CallNodeJS ( ) — alias, Params e os três blocos
TimeOut, Error e Success. Veja
DLL (integração) pra configuração, pasta das bibliotecas e o contrato que
a DLL precisa seguir.
Library é sempre "arquivo + função", num campo só: o arquivo da DLL (caminho a partir
de DLLBridge\Libraries, sem o .dll), um ponto e o nome da função dentro
dela — ex: Library: "ACBr/crud_acbr_nfe.Enviar". A chamada bloqueia até a
DLL devolver um resultado, com um limite de tempo (TimeOut, em segundos).
⚠️Alias e os três blocos são obrigatórios —
#CallDLL:alias(...) sempre precisa do alias, de Library (com a
função depois do ponto) e das três declarações, TimeOut, Error e
Success. Faltar qualquer um deles é erro de configuração do endpoint.
Success — a DLL devolveu 0 dentro do tempo. O JSON devolvido
fica disponível pelo alias (:alias.campo), e o status HTTP segue as regras normais
do endpoint (200/201/204).
TimeOut — o tempo limite passou sem resposta. O processo que estava
executando a DLL é encerrado e substituído, e o bloco roda normalmente. Como no
#CallNodeJS, não é erro por padrão: o status só muda se o próprio bloco chamar
#SetResponseCode ( ).
Error — a DLL devolveu um número diferente de 0, ou o processo
que a executava caiu durante a chamada. A mensagem fica disponível em
#DLL_Error(message).
Esse bloco força o status HTTP para 400 automaticamente — chamar
#SetResponseCode ( ) dentro dele substitui o 400 por outro
código.
O motor só força um erro 500 automático quando a invocação em si não consegue
acontecer — Library sem a função depois do ponto, biblioteca
não encontrada em DLLBridge\Libraries,
DLL de 32 bits ou DLL que não exporta CrudDLL_Call/CrudDLL_Free. Isso é
erro de configuração do endpoint, não um resultado de negócio.
#CallDLL:NFE(
Library: "ACBr/crud_acbr_nfe.Enviar",
Params: { idNota = :ID_NOTA, lote = :LOTE },
TimeOut: 40 {
update nota set status = 55 where nota = :ID_NOTA;
#SetVarResult(status = 'Aguardando Sefaz');
},
Error: {
update nota set status = 618 where nota = :ID_NOTA;
#SetVarResult(status = 'Nota fiscal rejeitada');
#SetVarResult(motivo = #DLL_Error(message));
},
Success: {
update nota set status = 60, chave_acesso = :NFE.chaveAcesso where nota = :ID_NOTA;
#SetVarResult(status = 'Emitida');
#SetVarResult(chave = :NFE.chaveAcesso);
}
);
⚠️A troca com a DLL é sempre JSON — nos dois sentidos. O servidor entrega
Params e o contexto como texto JSON (UTF-8), e a DLL devolve JSON em
AResultJson:
retorno 0 com um objeto (vira uma linha em
:alias.campo) ou um array de objetos (uma linha por item, pronto para
#For; itens que não são objeto são ignorados) vai para o Success.
Qualquer outra coisa com retorno 0 — texto que não é JSON (XML, INI, texto livre), um
valor JSON solto (texto, número, boolean) ou resposta vazia — vai para o Error, com
#DLL_Error(message) = "The DLL returned an invalid result (expected a JSON object or
array).".
Retorno diferente de 0 é sempre Error: a DLL devolve
{"message": "..."} e o texto de message vai para
#DLL_Error(message) (se não vier JSON, o próprio texto devolvido; se vier vazio,
"The DLL returned error code N.").
Ver Contrato obrigatório da DLL em DLL (integração).
Atenção de tipagem: assim como no #CallNodeJS, os valores de
Params chegam na DLL sempre como string dentro do JSON — a DLL
converte para número ou data quando precisar. O que a DLL devolve precisa ser um JSON válido (objeto
ou array); um texto que não é JSON vai para o bloco Error.
API externa
Macros para falar com plataformas externas direto do script — chamar a API e controlar
o ritmo das chamadas.
#CallHTTP ( ) — chama uma API externa (um ERP, uma
plataforma de e-commerce, um serviço de consulta) montando a URL e o body no próprio script, e trata o
sucesso, o erro e o timeout da chamada.
#IntervalMS ( ) — pausa o script por alguns
milissegundos entre uma chamada e a próxima, para evitar o erro de rate limited quando o
endpoint chama a mesma plataforma várias vezes (por exemplo, dentro de um
#For ( )).
#CallHTTP ( )
Script Suportado: Somente Endpoint
Chama uma API externa direto do script — um ERP, uma plataforma de e-commerce, um
serviço de consulta — sem Node.js e sem ferramenta à parte. O método, a
autenticação e os headers ficam na configuração do endpoint; o
próprio script monta a URL (com parâmetros e dados de alias) e o
body, e decide o que fazer em cada desfecho.
A chamada de saída é independente do endpoint: o tipo do endpoint e o
Result definem o que o seu cliente recebe, e isso não muda por causa do
#CallHTTP. A resposta da API externa é só um dado a mais dentro do script, que o script
usa como quiser (por exemplo, gravando no Result com
#SetVarResult ( )).
Comandos
Os comandos de preparação (#HttpNewUrl, #HttpAddUrl, #HttpParam,
#HttpHeader, #HttpBasic, #HttpBearer e #HttpApiKey) são escritos antes do #CallHTTP e valem só
para a próxima chamada: depois dela, e a cada volta de um #For ( ), tudo volta
ao que está salvo na Config API External. Os argumentos aceitam texto literal, :param,
:alias.campo e a concatenação com ||.
Comando
Exemplo
#CallHTTP:alias( ... );
Executa a chamada à API externa com o que foi preparado antes dele. Recebe o BodyPayload (opcional) e os três desfechos: TimeOut, Error e Success. A resposta fica no alias.
Troca a URL base da Config API External pelo endereço informado, só para a próxima chamada. Usado quando cada cliente tem o seu próprio endereço. O endereço entra como está.
#HttpNewUrl('https://' || :LOJAS.DOMINIO);
#HttpAddUrl(caminho);
Acrescenta um pedaço de caminho ao endereço (a URL base ou a do #HttpNewUrl). Repetido, vai acrescentando na ordem; a / entre os pedaços é colocada automaticamente. Valores de referência são codificados.
#HttpAddUrl('/produtos/saldodeestoque');
#HttpParam(nome, valor);
Acrescenta um parâmetro de query (?nome=valor) à próxima chamada. O valor é codificado. Mesmo nome declarado de novo, ou já fixo na configuração: vale o último.
#HttpParam('produto', :PRODUTO);
#HttpHeader(nome, valor);
Acrescenta um header à próxima chamada. Mesmo nome (sem diferenciar maiúsculas de minúsculas) substitui o anterior ou o fixo da configuração.
Troca a autorização da Config API External por Bearer com este token, só na próxima chamada.
#HttpBearer(:CLIENTES.TOKEN_API);
#HttpApiKey(nome, valor);
Troca a autorização da Config API External por uma chave de API com este nome e valor, só na próxima chamada. A chave vai no header ou na query, conforme marcado na configuração.
#HttpApiKey('X-Api-Key', :CLIENTES.CHAVE_API);
#Http_Error(message)
Só dentro do bloco Error: a descrição do erro.
#SetVarResult(motivo = #Http_Error(message));
#HttpReceivedStatus(:alias)
O status HTTP que a API externa devolveu na chamada daquele alias (200, 201, 404, 409...). Vale no Success, no Error e depois do #CallHTTP. Sem resposta (TimeOut ou falha de conexão): 0.
#SetVar(:status = #HttpReceivedStatus(:PED));
#HttpReceivedHeader(:alias, 'nome')
O valor de um header que a API externa devolveu na resposta daquele alias (ex.: total de registros, se há mais páginas). O nome do header não diferencia maiúsculas de minúsculas. Vale no Success, no Error e depois do #CallHTTP. Se o header não veio, ou não houve resposta: vazio.
Dentro do #CallHTTP, opcional: o corpo da requisição, em Json, Form ou Text. O Content-Type é colocado automaticamente. Um alias usado direto vira um array JSON.
BodyPayload: Json {
numero = :NUMERO_PEDIDO,
cliente = { id = :COD_CLIENTE, nome = :NOME_CLIENTE },
total = :VALOR_TOTAL,
itens = :ITENS
}
Status HTTP recebido — #HttpReceivedStatus
Muitas plataformas respondem com códigos diferentes conforme a situação: 409 quando o
pedido não pode ser alterado, 404 quando não existe, e assim por diante.
#HttpReceivedStatus(:alias) devolve o código que chegou na chamada daquele alias, para o
script decidir o que fazer em cada caso. O valor pode ir direto num #case ou
#iif, ou ser guardado com #SetVar ( ).
#HttpNewUrl('https://api.exemplo.com.br/pedidos');
#HttpAddUrl('/' || :NUMERO || '/cancelar');
#CallHTTP:PED(
TimeOut: 15 { #SetResponseCode(504); },
Error: {
#case(#HttpReceivedStatus(:PED) of
409: {
#SetVarResult(MOTIVO = 'Pedido nao pode mais ser cancelado');
#SetResponseCode(409);
},
404: {
#SetVarResult(MOTIVO = 'Pedido nao encontrado na loja');
#SetResponseCode(404);
},
ELSE: {
#SetVarResult(MOTIVO = #Http_Error(message));
#SetResponseCode(502);
}
);
},
Success: {
update PEDIDO set CANCELADO = 'T' where NUMERO = :NUMERO;
}
);
No Error, o #case separa cada código recebido: 409 e 404 viram uma mensagem
própria e devolvem o mesmo status para quem chamou o endpoint; qualquer outro erro usa a descrição do
#Http_Error(message) e devolve 502. No Success o código também está
disponível, por exemplo para diferenciar 200 de 201.
Header recebido — #HttpReceivedHeader
Algumas APIs mandam informações nos headers da resposta, e não no corpo: o total de
registros, se ainda existem mais páginas, o limite de chamadas restante.
#HttpReceivedHeader(:alias, 'nome') devolve o valor desse header na última chamada daquele
alias, como texto (ou número, quando o valor é numérico). Se o header não veio, ou se a chamada não teve
resposta, devolve vazio, sem erro. O valor pode ir num #iif, num #case, no
limite de um #For ou ser guardado com #SetVar ( ).
O script traz só o que muda a cada execução: o caminho e a query da URL, com
#HttpAddUrl e #HttpParam, e os três desfechos. O resto vem da configuração do endpoint (Config API External), descrita a seguir.
Configuração do request (no endpoint)
O que é fixo na chamada fica no próprio endpoint, na tela Config API External
(botão ao lado de Anonymous Access, em endpoints de script do tipo Json). Cada endpoint tem
uma configuração, usada por todos os #CallHTTP do seu script. O alias
do comando (#CallHTTP:SALDO) não escolhe configuração: ele é só o nome em memória da
resposta, lida depois como :SALDO.campo, igual ao alias de um
select:alias.
A tela guarda:
Method — GET, POST, PUT,
PATCH ou DELETE.
URL base — o endereço de partida da chamada (por exemplo,
https://api.exemplo.com.br/v1). O script acrescenta o caminho com
#HttpAddUrl ou troca a base com #HttpNewUrl.
Authorization — o tipo (sem autenticação, Basic com usuário e
senha, Bearer com token, ou chave de API com nome e valor, enviada no header ou na
query). O script pode trocá-la a cada chamada com #HttpBasic, #HttpBearer
ou #HttpApiKey.
Params — parâmetros de query fixos, em uma tabela de chave e valor, cada um
podendo ser ligado ou desligado.
Headers — headers fixos, na mesma forma de tabela. Os mais comuns
(Content-Type, Accept…) podem ser escolhidos numa lista.
SSL/TLS — as versões do protocolo aceitas na conexão. Sem nenhuma marcada, vale
o padrão do Windows; só é preciso mexer para APIs com exigência específica.
A tela também tem um botão Send que faz uma chamada de teste com a configuração
e mostra a resposta, sem executar o script. Ela vai junto com o endpoint na sincronização com o
servidor.
Um endpoint sem configuração assume o padrão: GET, sem autenticação, sem URL base
(o endereço inteiro vem do #HttpNewUrl).
Parâmetros do comando
BodyPayload — opcional; o corpo da requisição, em Json,
Form ou Text (ver abaixo). O Content-Type é colocado
automaticamente.
TimeOut, Error e Success — os três desfechos
(ver abaixo).
O endereço não é um parâmetro do comando: ele é preparado antes do
#CallHTTP, com #HttpNewUrl, #HttpAddUrl e
#HttpParam (ver "Montagem da URL").
Montagem da URL
A URL é montada no momento da chamada, a partir da URL base salva na
Config API External, por três comandos escritos antes do #CallHTTP:
#HttpNewUrl(endereco); — troca a URL base da próxima chamada, para atender um
cliente que tem outro endereço. O endereço entra como está.
#HttpAddUrl(caminho); — acrescenta um pedaço de caminho ao endereço. Repetido, vai
acrescentando na ordem; a / entre a base e o pedaço é colocada automaticamente.
#HttpParam(nome, valor); — acrescenta um parâmetro de query
(?nome=valor&...).
Os argumentos aceitam texto literal, parâmetros do endpoint
(:ID_PEDIDO), campos de um alias (:CLIENTE.codigo —
dentro de um #For ( ), o campo da linha da vez) e a concatenação com
||:
O servidor codifica os valores que entram por referência no
#HttpAddUrl e no #HttpParam, e mantém como estão os textos que você
escreveu (/, ?, &, =), então um valor com
espaço, acento ou & não quebra o endereço. É isso que torna o recurso irrestrito:
qualquer dado que o script já tem — parâmetro, consulta anterior, retorno de outra chamada — pode
virar parte da URL.
Valores por chamada: headers, parâmetros e URL (proposta)
📝Proposta — a sintaxe e as regras desta seção ainda estão sujeitas a aprovação e podem mudar
antes da implementação.
A tela Config API External guarda os valores fixos do request: o
método, a URL, a autorização, os parâmetros, os headers e o body. Eles valem para toda chamada.
Quando o mesmo endpoint precisa consultar clientes diferentes — por exemplo, cada
cliente tem o seu próprio token de acesso no Authorization —, o script pode
acrescentar ou substituir headers, parâmetros e partes da URL a cada chamada, sem
precisar de um endpoint por cliente.
Macros de contexto da chamada
São comandos independentes, escritos antes do #CallHTTP que vão
afetar (por isso terminam com ;). Eles só preparam o contexto: nada é
enviado à API até o #CallHTTP rodar — é o #CallHTTP que executa a
requisição, com o que foi preparado.
#HttpHeader(nome, valor); — acrescenta um header à próxima chamada.
#HttpParam(nome, valor); — acrescenta um parâmetro de query à próxima chamada. O
valor é codificado pelo servidor.
#HttpNewUrl(endereco); e #HttpAddUrl(caminho); — trocam a URL base e
acrescentam caminho à próxima chamada (ver "Montagem da URL").
Os argumentos aceitam um texto literal ou uma referência (:param,
:alias.campo).
Sintaxe — duas propostas para análise
Há duas formas de escrever os mesmos comandos. Os exemplos desta seção usam a Proposta
A; as duas fazem exatamente a mesma coisa.
Proposta A — nome e valor separados por vírgula, como uma chamada de função:
Proposta B — nome = valor, o mesmo formato do
#Call ( ) e do #SetVarResult ( );
um nome com hífen vai entre aspas, e vários itens podem ir no mesmo comando, separados por
vírgula:
Também está em análise o nome dos comandos: #HttpHeader,
#HttpParam (usados aqui) ou HeaderAdd e
ParamAdd. O #HttpNewUrl e o #HttpAddUrl recebem um único
argumento e são escritos igual nas duas propostas. Qualquer combinação de nome e sintaxe vale; as regras
abaixo (contexto zerado a cada chamada e o último valor da mesma chave vence) não mudam.
Mais de um header ou parâmetro
Cada comando acrescenta um item. Para ter vários headers ou vários parâmetros,
repita o comando, uma linha por item, antes do #CallHTTP. Não há limite de itens, e a
ordem das linhas não importa:
Se a mesma chave for declarada duas vezes antes da chamada, vale o último valor
declarado — o valor anterior é substituído, não duplicado. Isso vale também para a chave que já
existe nos valores fixos da configuração.
Contexto zerado a cada chamada
A cada chamada e a cada volta do #For ( ),
o contexto da requisição é zerado: a URL, os parâmetros e os headers voltam aos
valores fixos salvos na tela de configuração, e só entra o que o script declarou
naquela volta. Nada que foi declarado para um cliente vaza para o seguinte.
Como o contexto é zerado também depois de cada #CallHTTP, quem faz duas chamadas para o
mesmo cliente na mesma volta declara os valores antes de cada uma.
Header ou parâmetro com a mesma chave de um valor fixo
Quando o script declara um header (ou parâmetro) com a mesma chave de um valor
fixo, o valor do script substitui o fixo naquela chamada. O header é comparado sem
diferenciar maiúsculas de minúsculas (authorization e Authorization são o
mesmo header). Uma chave que não existe na configuração é simplesmente acrescentada.
Exemplo — um token de acesso por cliente
Na configuração do request ficam o método, a URL base e os headers comuns a todos os clientes. O
Authorization não é preenchido ali, porque muda de cliente para cliente:
select:CLIENTES CODIGO, LOJA, TOKEN_API, CHAVE_API, URL_API
from CLIENTE where INTEGRACAO_ATIVA = 'T';
#For() :CLIENTES do
#begin
-- o contexto volta aos valores fixos no início de cada volta
#HttpHeader('Authorization', 'Bearer ' || :CLIENTES.TOKEN_API);
#HttpHeader('X-Api-Key', :CLIENTES.CHAVE_API);
#HttpParam('cliente', :CLIENTES.CODIGO);
#HttpParam('loja', :CLIENTES.LOJA);
#HttpAddUrl('/' || :CLIENTES.LOJA || '/produtos/saldodeestoque');
#CallHTTP:SALDO(
TimeOut: 15 {
update CLIENTE set SALDO_STATUS = 'timeout' where CODIGO = :CLIENTES.CODIGO;
},
Error: {
update CLIENTE set SALDO_STATUS = 'erro' where CODIGO = :CLIENTES.CODIGO;
},
Success: {
update CLIENTE set SALDO_STATUS = 'ok' where CODIGO = :CLIENTES.CODIGO;
}
);
#end;
Repare no #HttpAddUrl: a loja do cliente da volta (:CLIENTES.LOJA) entra no
caminho, então o mesmo endpoint atende lojas distintas.
Se cada cliente também tiver o seu próprio endereço, acrescente
#HttpNewUrl(:CLIENTES.URL_API); antes do #HttpAddUrl.
BodyPayload
O tipo do body define o formato e o Content-Type:
-- JSON, com campos escritos no script
BodyPayload: Json {
numero = :NUMERO_PEDIDO,
cliente = { id = :COD_CLIENTE, nome = :NOME_CLIENTE },
total = :VALOR_TOTAL,
itens = :ITENS
}
-- formulário (application/x-www-form-urlencoded)
BodyPayload: Form { grant_type = 'client_credentials', client_id = {CLIENT_ID}, client_secret = {CLIENT_SECRET} }
-- texto livre, com o Content-Type informado (ex.: XML)
BodyPayload: Text 'application/xml' { <nfe><chave>:CHAVE</chave></nfe> }
Usando um alias direto
Um alias pode ser usado direto, como valor de um campo ou como o body inteiro. Ele
vira um array JSON, uma posição por linha, com todos os campos, o número como número
e a data em ISO 8601 (o mesmo resultado de #SetFormatJson ( ) sem
projeção):
-- o alias como valor de um campo
BodyPayload: Json { numero = :NUMERO_PEDIDO, itens = :ITENS }
-- o alias como body inteiro (o corpo é o próprio array)
BodyPayload: Json :ITENS
As chaves do JSON são os nomes das colunas do alias. Para controlar o nome, escreva o
select que monta o alias já com o nome desejado
(select:ITENS cod_produto as sku, qtde as quantidade, preco from ...). Para escolher e
renomear os campos no próprio body, use o #SetFormatJson:
O tipo de cada valor segue o tipo declarado do parâmetro do endpoint:
INTEGER, BIGINT, DECIMAL e DOUBLE PRECISION saem
como número, e texto sai como texto — sem isso, uma API que espera
"total": 150.50 receberia "total": "150.50". Um valor escrito direto no
script segue o literal: 10.5 sai como número e 'abc' como texto.
Success, TimeOut e Error
Success — a API respondeu dentro do tempo com um status 2xx. O
retorno fica disponível pelo alias declarado no comando
(:RESP.campo), e a resposta inteira, em JSON, com
#SetFormatJson(:RESP). O status HTTP do seu endpoint segue as regras normais
(200/201/204), de acordo com o que o bloco grava e devolve.
TimeOut — o tempo limite (em segundos) passou sem resposta. Não é erro por
padrão: a chamada pode terminar depois, só não dentro desta requisição. O status do endpoint também
segue as regras normais, a menos que o bloco chame
#SetResponseCode ( ).
Error — a chamada não deu certo: falha de rede, certificado inválido, ou a API
respondeu com um status fora de 2xx (4xx, 5xx). Dentro do bloco,
#Http_Error(message) traz a descrição do erro. Esse bloco
força o status HTTP para 400; #SetResponseCode dentro do bloco
substitui esse código por outro.
Um erro de configuração — URL inválida, variável não declarada no endpoint — é um
erro 500 do endpoint, não um desfecho de negócio a tratar no script.
BodyPayload e a resposta: o que você envia e o que você recebe
O BodyPayload: escrito dentro do
#CallHTTP é o corpo que você envia para a API. O
alias guarda a resposta que você recebe: depois de um
Success, use :alias.campo para ler um campo da resposta e
#SetFormatJson(:alias) para a resposta inteira em JSON.
Exemplo — devolver a resposta da API
Para o seu endpoint devolver o JSON da API externa, grave o #SetFormatJson(:alias)
num campo do Result declarado como TEXT. O JSON é montado a partir dos campos da resposta. Uma resposta JSON vai como JSON de verdade, e
não como texto entre aspas:
Com o endpoint configurado na Config API External como POST, autorização
Bearer:
select:ITENS cod_produto as sku, qtde as quantidade, preco
from PEDIDO_ITENS where PEDIDO = :NUMERO_PEDIDO;
#HttpAddUrl('/pedidos');
#CallHTTP:PEDIDO(
BodyPayload: Json {
numero = :NUMERO_PEDIDO,
cliente = { id = :COD_CLIENTE, nome = :NOME_CLIENTE },
total = :VALOR_TOTAL,
itens = :ITENS
},
TimeOut: 20 {
update PEDIDO set STATUS = 55 where PEDIDO = :NUMERO_PEDIDO;
#SetVarResult(status = 'Aguardando a plataforma');
},
Error: {
update PEDIDO set STATUS = 618 where PEDIDO = :NUMERO_PEDIDO;
#SetVarResult(status = 'Pedido rejeitado');
#SetVarResult(motivo = #Http_Error(message));
},
Success: {
update PEDIDO set STATUS = 60, PROTOCOLO = :PEDIDO.protocolo where PEDIDO = :NUMERO_PEDIDO;
#SetVarResult(status = 'Enviado');
#SetVarResult(protocolo = :PEDIDO.protocolo);
}
);
Exemplo — uma chamada por linha (#For)
Dentro de um #For ( ), o #CallHTTP roda uma vez por linha do
alias, e os campos da linha da vez entram direto no body e nos blocos de desfecho
(:PEDIDOS.campo). Cada chamada trata o seu próprio sucesso, erro e timeout:
select:PEDIDOS PEDIDO, NUMERO, VALOR_TOTAL
from PEDIDO where STATUS = 50;
#For() :PEDIDOS do
#begin
#HttpAddUrl('/pedidos');
#CallHTTP:PEDIDO(
BodyPayload: Json { numero = :PEDIDOS.NUMERO, total = :PEDIDOS.VALOR_TOTAL },
TimeOut: 20 {
update PEDIDO set STATUS = 55 where PEDIDO = :PEDIDOS.PEDIDO;
},
Error: {
update PEDIDO set STATUS = 618 where PEDIDO = :PEDIDOS.PEDIDO;
},
Success: {
update PEDIDO set STATUS = 60, PROTOCOLO = :PEDIDO.protocolo where PEDIDO = :PEDIDOS.PEDIDO;
}
);
#end;
Muitas plataformas limitam quantas requisições aceitam por período, e um laço dispara uma chamada
logo depois da outra. Para espaçar as chamadas e evitar o erro de rate limited, use o
#IntervalMS ( ) dentro do laço.
Exemplo — buscando os pedidos direto da Shopify (GET com #CallHTTP)
No exemplo de importação da Shopify (em #SetImportJsonArray ( )) o pedido já estava gravado no banco. Aqui o próprio endpoint vai buscar
os pedidos na Shopify (um GET) e processa a resposta na hora. Cada loja tem o
seu endereço e o seu token de acesso, guardados numa tabela do sistema, então o mesmo endpoint
atende várias lojas — com o #HttpNewUrl e o #HttpHeader declarados a
cada volta (ver "Valores por chamada", acima). Na configuração
do request ficam só o método GET e a URL base padrão.
Preparação — para reproduzir o exemplo
Para rodar o exemplo no seu ambiente, crie as tabelas abaixo (ajuste os tipos ao seu banco) e
configure o request e o endpoint como descrito.
create table SHOPIFY_LOJA (
CODIGO integer primary key,
DOMINIO varchar(120), -- ex.: minhaloja.myshopify.com
TOKEN_API varchar(200), -- token de acesso da loja (Admin API, permissão de leitura de pedidos)
ULTIMO_ID bigint default 0,
ATIVA char(1) default 'T',
STATUS_IMPORTACAO varchar(20)
);
create table PEDIDO_VENDA (
ID_PEDIDOV integer primary key, -- gerado pelo banco
COD_PEDIDOV bigint, -- número do pedido na Shopify
NAME varchar(30),
VALOR_TOTAL decimal(15,2),
DATA_PEDIDO timestamp,
LOJA integer
);
create table PEDIDO_ITENS (
ID_ITEM integer primary key,
ID_PEDIDOV integer,
PRODUTO varchar(200),
SKU varchar(60),
QUANTIDADE integer,
VALOR decimal(15,2)
);
-- uma loja de teste
insert into SHOPIFY_LOJA (CODIGO, DOMINIO, TOKEN_API)
values (1, 'minhaloja.myshopify.com', 'shpat_xxxxxxxxxxxxxxxx');
Endpoint — crie um endpoint do tipo Json com o método GET e cole o
script abaixo.
Config API External — método GET, sem autorização, e a URL base
padrão com o endereço de uma das suas lojas (o #HttpNewUrl a troca por loja).
Token — na Shopify, crie um app personalizado para a loja e copie o token de
acesso da Admin API; ele vai na coluna TOKEN_API.
select:LOJAS CODIGO, DOMINIO, TOKEN_API, ULTIMO_ID
from SHOPIFY_LOJA where ATIVA = 'T';
#For() :LOJAS do
#begin
-- o contexto volta aos valores fixos no início de cada volta
#HttpNewUrl('https://' || :LOJAS.DOMINIO);
#HttpAddUrl('/admin/api/2024-10/orders.json');
#HttpParam('status', 'any');
#HttpParam('limit', 50);
#HttpParam('since_id', :LOJAS.ULTIMO_ID);
#HttpHeader('X-Shopify-Access-Token', :LOJAS.TOKEN_API);
#CallHTTP:RESP(
TimeOut: 20 {
update SHOPIFY_LOJA set STATUS_IMPORTACAO = 'timeout' where CODIGO = :LOJAS.CODIGO;
},
Error: {
update SHOPIFY_LOJA set STATUS_IMPORTACAO = 'erro' where CODIGO = :LOJAS.CODIGO;
},
Success: {
-- a resposta é {"orders":[ ... ]}: o array está na chave "orders", no primeiro nível
#SetImportJsonArray(:pedidos, #SetFormatJson(:RESP), 'orders');
#For() :pedidos do
#begin
#SetImportJsonArray(:itens, #SetFormatJson(:pedidos), 'line_items');
insert:newpedido into PEDIDO_VENDA (COD_PEDIDOV, NAME, VALOR_TOTAL, DATA_PEDIDO, LOJA)
values (:pedidos.id, :pedidos.name, #SetToFloat(:pedidos.total_price),
#SetISO8601ToDateTime(:pedidos.created_at), :LOJAS.CODIGO)
#return(ID_PEDIDOV);
#For() :itens do
#begin
insert into PEDIDO_ITENS (PRODUTO, SKU, QUANTIDADE, VALOR, ID_PEDIDOV)
values (:itens.title, :itens.sku, :itens.quantity,
#SetToFloat(:itens.price), :newpedido.ID_PEDIDOV);
#end;
-- guarda até onde já importou: a próxima execução pede só os pedidos mais novos
update SHOPIFY_LOJA set ULTIMO_ID = :pedidos.id where CODIGO = :LOJAS.CODIGO;
#end;
update SHOPIFY_LOJA set STATUS_IMPORTACAO = 'ok' where CODIGO = :LOJAS.CODIGO;
}
);
-- a Shopify limita as requisições por segundo: espaça uma loja da outra
#IntervalMS(500);
#end;
O select:LOJAS traz as lojas ativas, e o #For :LOJAS faz uma chamada por
loja. Em cada volta, o #HttpNewUrl, o #HttpAddUrl e o
#HttpParam montam o endereço, e o #HttpHeader coloca o token
daquela loja, e o #CallHTTP executa o GET. O
since_id pede só os pedidos com número maior que o último já importado, por isso rodar o
endpoint de novo não grava o mesmo pedido duas vezes.
A resposta da Shopify é um objeto, {"orders": [ ... ]}. O
#SetImportJsonArray(:pedidos, #SetFormatJson(:RESP), 'orders') procura a chave
orders no primeiro nível e materializa os pedidos num alias, um por linha. Dentro do
laço de pedidos, a segunda chamada, sobre line_items, traz os itens do pedido da vez,
como no exemplo de importação da Shopify. O #return(ID_PEDIDOV) do cabeçalho vira a chave de cada item.
A resposta entra no #SetImportJsonArray como #SetFormatJson(:RESP);
um campo da resposta continua sendo :RESP.campo.
Os valores da Shopify chegam como texto — "total_price": "78.42" e
"price": "69.90" — e por isso passam pelo #SetToFloat. O
created_at vem em ISO 8601 (2024-05-10T14:30:00-03:00) e é lido pelo
#SetISO8601ToDateTime, que ignora o deslocamento do fuso (ver as macros de data em
JSON). O resultado de cada loja fica em STATUS_IMPORTACAO: o
Success grava 'ok', e o Error e o TimeOut deixam
a loja marcada para o administrador ver, sem impedir as próximas lojas.
Exemplo — publicando produtos na Shopify (POST com #CallHTTP)
O caminho inverso do exemplo anterior: o sistema tem produtos cadastrados e ainda não publicados, e o
endpoint envia cada um para a loja da Shopify (um POST). A Shopify
devolve o id do produto criado, e o endpoint o guarda para saber que aquele produto já
foi publicado — a próxima execução envia só os que faltam.
Preparação — para reproduzir o exemplo
Use a tabela SHOPIFY_LOJA do exemplo anterior. O token da loja precisa de permissão de
escrita de produtos (write_products). Crie também as tabelas abaixo:
create table PRODUTO (
ID_PRODUTO integer primary key,
LOJA integer, -- CODIGO da loja em SHOPIFY_LOJA
NOME varchar(200),
DESCRICAO varchar(2000), -- aceita HTML simples
MARCA varchar(100),
CATEGORIA varchar(100),
PUBLICADO char(1) default 'F',
SHOPIFY_ID bigint,
STATUS_PUBLICACAO varchar(20),
ERRO_PUBLICACAO varchar(2000)
);
create table PRODUTO_VARIANTE (
ID_VARIANTE integer primary key,
ID_PRODUTO integer,
SKU varchar(60),
PRECO decimal(15,2),
OPCAO varchar(60) -- ex.: tamanho ou cor
);
-- um produto de teste, com duas variantes
insert into PRODUTO (ID_PRODUTO, LOJA, NOME, DESCRICAO, MARCA, CATEGORIA)
values (1, 1, 'Camiseta Básica', '<p>Camiseta de algodão.</p>', 'Marca Exemplo', 'Camisetas');
insert into PRODUTO_VARIANTE (ID_VARIANTE, ID_PRODUTO, SKU, PRECO, OPCAO) values (1, 1, 'CAM-001-P', 69.90, 'P');
insert into PRODUTO_VARIANTE (ID_VARIANTE, ID_PRODUTO, SKU, PRECO, OPCAO) values (2, 1, 'CAM-001-M', 69.90, 'M');
Na Config API External, o método é POST, sem autorização, e a URL base
padrão é o endereço de uma das suas lojas (o #HttpNewUrl a troca por loja). O Content-Type é colocado automaticamente
pelo BodyPayload: Json.
select:PRODUTOS p.ID_PRODUTO, p.NOME, p.DESCRICAO, p.MARCA, p.CATEGORIA,
l.DOMINIO, l.TOKEN_API
from PRODUTO p
join SHOPIFY_LOJA l on l.CODIGO = p.LOJA
where p.PUBLICADO = 'F' and l.ATIVA = 'T';
#For() :PRODUTOS do
#begin
-- as chaves do JSON são os nomes das colunas do alias
select:VARIANTES SKU as sku, PRECO as price, OPCAO as option1
from PRODUTO_VARIANTE where ID_PRODUTO = :PRODUTOS.ID_PRODUTO;
-- o contexto volta aos valores fixos no início de cada volta
#HttpNewUrl('https://' || :PRODUTOS.DOMINIO);
#HttpAddUrl('/admin/api/2024-10/products.json');
#HttpHeader('X-Shopify-Access-Token', :PRODUTOS.TOKEN_API);
#CallHTTP:RESP(
BodyPayload: Json {
product = {
title = :PRODUTOS.NOME,
body_html = :PRODUTOS.DESCRICAO,
vendor = :PRODUTOS.MARCA,
product_type = :PRODUTOS.CATEGORIA,
status = 'active',
variants = :VARIANTES
}
},
TimeOut: 20 {
-- a Shopify pode ter criado o produto: não reenvie sem conferir
update PRODUTO set STATUS_PUBLICACAO = 'verificar' where ID_PRODUTO = :PRODUTOS.ID_PRODUTO;
},
Error: {
update PRODUTO set STATUS_PUBLICACAO = 'erro', ERRO_PUBLICACAO = #Http_Error(message)
where ID_PRODUTO = :PRODUTOS.ID_PRODUTO;
},
Success: {
update PRODUTO set PUBLICADO = 'T', STATUS_PUBLICACAO = 'ok', SHOPIFY_ID = :RESP.product.id
where ID_PRODUTO = :PRODUTOS.ID_PRODUTO;
}
);
-- a Shopify limita as requisições por segundo
#IntervalMS(500);
#end;
O primeiro select traz os produtos ainda não publicados, junto com o endereço e o token
da loja de cada um. Em cada volta do #For, o select:VARIANTES monta as
variantes do produto da vez já com os nomes que a Shopify espera (sku,
price, option1), e o BodyPayload: Json usa esse alias direto
como a lista variants: o preço sai como número e cada variante vira um objeto, como
descrito em "Usando um alias direto". O restante do product são campos do próprio
produto.
A Shopify responde com o produto criado, {"product": {"id": ..., "variants": [ ... ]}}.
O Success grava o id em SHOPIFY_ID e marca o produto como
publicado. No Error, o #Http_Error(message) guarda a mensagem do erro em
ERRO_PUBLICACAO para o usuário corrigir o cadastro. O TimeOut não é erro:
o produto pode ter sido criado depois do limite, por isso fica como 'verificar' em vez
de voltar para a fila — reenviar sem conferir poderia duplicar o produto na loja.
Duas propostas para ler o id da resposta (em análise): a
Proposta A, usada acima, é :RESP.product.id, que desce pelos campos do
objeto de resposta; a Proposta B é materializar o objeto com
#SetImportJsonArray(:criado, #SetFormatJson(:RESP), 'product') e ler
:criado.id, como nos outros exemplos de JSON.
#IntervalMS ( )
Script Suportado: Somente Endpoint
Macro importante para evitar o erro de rate limited (normalmente
HTTP 429 Too Many Requests) das plataformas externas. Ela pausa o script por um número de
milissegundos antes de seguir para o próximo comando: quando o endpoint chama a mesma
plataforma várias vezes — o caso típico é um #CallHTTP ( )dentro de um #For ( ), uma chamada por linha — o
#IntervalMS espaça uma chamada da outra, para não passar do limite de requisições que a
plataforma aceita.
#IntervalMS(milissegundos);
milissegundos aceita um número literal ou uma referência (:param,
:alias.campo), e precisa resolver para um número inteiro entre
0 e 60000. Fora disso, o endpoint responde
HTTP 500 com o erro de configuração. É um comando
independente, por isso termina com ;.
Exemplo — #CallHTTP dentro de um #For, com intervalo entre as chamadas
Uma consulta de saldo por produto, uma chamada por linha, com meio segundo entre elas:
select:PRODUTOS PRODUTO from PRODUTO where ATIVO = 'T';
#For() :PRODUTOS do
#begin
#HttpAddUrl('/produtos/saldodeestoque');
#HttpParam('produto', :PRODUTOS.PRODUTO);
#CallHTTP:SALDO(
TimeOut: 15 {
update PRODUTO set SALDO_STATUS = 'timeout' where PRODUTO = :PRODUTOS.PRODUTO;
},
Error: {
update PRODUTO set SALDO_STATUS = 'erro' where PRODUTO = :PRODUTOS.PRODUTO;
},
Success: {
update PRODUTO set SALDO_PLATAFORMA = :SALDO.saldo, SALDO_STATUS = 'ok'
where PRODUTO = :PRODUTOS.PRODUTO;
}
);
#IntervalMS(500);
#end;
A pausa de 500 ms acontece depois de cada chamada, antes da seguinte. Como o valor pode vir de um
parâmetro, dá para deixar o espaçamento configurável sem mexer no script:
#IntervalMS(:PAUSA_MS);.
Cuidados
O #IntervalMS funciona em qualquer parte do script — não só junto do
#CallHTTP: serve igualmente para espaçar chamadas de
#Call ( ) e #CallNodeJS ( ) dentro de um laço.
A pausa segura a requisição — e, se o script já abriu uma transação, a conexão
com o banco continua ocupada durante a espera. Use valores pequenos; o teto de 60 segundos existe
justamente para um valor errado não travar o servidor.
Se a requisição for cancelada pelo servidor (Task Monitor) durante a pausa, ela termina na hora,
sem esperar o tempo todo.
Não é suportado dentro do script de um ObjectArray.
Para espaçar chamadas de endpoints diferentes agendados pelo servidor, o
intervalo é configurado no próprio arquivo: ver a chave IntervalEndpointsMS do
SingleThreadScheduler.ini.
#SetImportJsonArray ( )
Script Suportado: Somente Endpoint
Importa um array JSON pra dentro de um alias novo em memória, iterável por
#For como qualquer outro alias — de duas formas, escolhidas pelo
3º argumento: ou o texto já é o array pronto e a macro só materializa, ou o array
está escondido dentro de um JSON maior e ela primeiro localiza (por nome de
chave) e só depois materializa. Diferente de um ObjectArray, não
precisa de nenhuma declaração prévia no endpoint — o formato do array só é conhecido em tempo de
execução.
Importante: #SetImportJsonArray nunca escreve no Result sozinho — ele
só materializa o array numa alias em memória. Seu uso mais comum é alimentar,
depois, um campo OBJECTARRAY declarado no Result — mas isso é sempre um passo A
MAIS, com #SetVarResult ( ) +
#SetFormatJson ( ) por cima da alias que ele acabou de criar (ver
exemplo completo mais abaixo).
Os três argumentos são sempre obrigatórios — uma assinatura fixa só, sem variação
de quantidade, pra nunca deixar dúvida sobre quantos parâmetros a macro espera de chamada pra
chamada:
:alias_novo — nome do alias que passa a existir a partir dessa linha, usado
exatamente como qualquer alias multi-linha.
fonte_json — o texto JSON de origem (ex: :RESP.itens depois de um
#CallNodeJS, uma coluna que já guarda um array, ou um JSON bem maior de onde o
array ainda precisa ser extraído). Precisa ser JSON: um campo
(:alias.campo), um :param ou #SetFormatJson(:alias).
O alias sozinho não é aceito — #SetImportJsonArray(:x, :USR, 'address')
é bloqueado no Save; o certo é #SetImportJsonArray(:x, #SetFormatJson(:USR), 'address').
ArrayName — o nome da chave a procurar dentro de fonte_json, ou
a palavra-chave null quando fonte_json já é o array pronto e não
precisa de nenhuma busca. Ex.: #SetImportJsonArray(:alias_novo, fonte_json, null);
Cada chamada substitui o conteúdo anterior do alias — se
#SetImportJsonArray for chamado de novo pro mesmo nome (ex: dentro de um
#For, uma vez por volta), o alias é resetado antes de ser populado, nunca acumula
linha de uma chamada em cima da outra.
Por que o 3º argumento é interessante: filtra e importa numa chamada só
Quando ArrayName não é null, a macro primeiro busca
essa chave dentro de fonte_json — mas só no primeiro nível, nunca
descendo sozinha em profundidade — e só depois materializa o que achou como
alias. Resolve o caso de integrações externas (marketplaces, plataformas de e-commerce, ERPs de
terceiros) cujo formato de retorno varia: em vez de escrever o caminho inteiro até o campo, basta
informar o nome da chave naquele nível.
-- fonte_json JA' e' o array (nada pra procurar)
#SetImportJsonArray(:itens, :RESP.itens, null);
-- fonte_json e' um JSON MAIOR, o array esta' escondido na chave "line_items"
#SetImportJsonArray(:itens, :pedido.corpo_json, 'line_items');
Se a chave não existir naquele nível, o resultado é um alias vazio — nunca erro (a estrutura de
uma integração externa pode legitimamente não trazer aquele campo numa chamada específica).
Se a chave encontrada for um objeto (e não um array), ele vira um alias de
uma linha. Útil para ler um campo de dentro de um objeto aninhado, como o endereço
de um usuário:
Já com null no 3º argumento, a fonte precisa ser um array:
#SetImportJsonArray(:ENDER, :USR.address, null), com o address sendo um objeto,
interrompe o script com HTTP 500.
Por que só o primeiro nível, de propósito
Um JSON real (o de um pedido Shopify, por exemplo) costuma repetir o mesmo nome de chave em
blocos completamente diferentes — no pedido da Shopify, presentment_money aparece
dezenas de vezes (dentro de total_price_set, de subtotal_price_set, de
cada line_items[].price_set, etc.). Uma busca automática que descesse sozinha até
achar a primeira ocorrência seria ambígua — pegaria qualquer uma ao acaso, não necessariamente a
certa. Por isso a macro só olha um nível de cada vez: pra chegar numa chave mais funda, o
desenvolvedor desce de propósito, chamando a macro de novo sobre o resultado do
nível anterior (guardado em memória no alias) — sempre sabendo exatamente qual ocorrência está
pegando. Como o resultado de uma chamada é sempre um alias (não texto), pra usar
como fonte_json na próxima chamada é preciso serializar de novo com
#SetFormatJson(:alias) — o alias sozinho não resolve pra texto
JSON por si só (ver exemplo "descendo nível a nível" mais abaixo).
Exemplo — consumindo um array que veio do Node
Um módulo Node que consulta um pedido num sistema externo e devolve, junto com os dados do
cabeçalho, uma lista de itens (RESP.itens). O array bruto costuma trazer campos
internos do sistema externo que não fazem sentido devolver pro cliente da API — é por isso que
#SetImportJsonArray só coloca o array em memória; quem decide quais campos realmente
saem na resposta é o #SetFormatJson logo depois (ver
#SetVarResult ( ), seção "Como escrever em um campo de um
ObjectArray para ser enviado no Result"):
A partir daí, :itens existe como qualquer outro alias — #For :itens do ... #end;
itera cada linha, e :itens.produto/:itens.qtd funcionam dentro do laço.
Lendo um array dentro de um alias já em memória
fonte_json não precisa vir só de :RESP — pode ser qualquer texto JSON já
resolvido, inclusive o de um alias que já foi importado antes. Nesse caso, use
#SetFormatJson(:alias) pra transformar o alias inteiro em JSON de novo —
o alias sozinho (:alias_novo, sem .campo) não resolve pra texto JSON por
si só, precisa passar pelo #SetFormatJson:
#SetImportJsonArray(:alias_novo, fonte_json, null);
#SetImportJsonArray(:alias_novo_2, #SetFormatJson(:alias_novo), null); -- importando um array dentro de outro array
O mesmo mecanismo serve pro caso de um array dentro de cada linha de outro array —
ex: uma lista de itens de pedido, onde cada item tem sua própria lista de descontos aplicados. Pra
ler isso, importe o array de fora primeiro, e dentro do #For que itera cada
linha, importe de novo — agora o array aninhado daquela linha específica — pra um alias interno:
#SetImportJsonArray(:itens, :RESP.itens, null);
#for :itens
do
#begin
#SetImportJsonArray(:descontos, :itens.descontos, null);
#for :descontos
do
#begin
insert into PEDIDO_ITEM_DESCONTO (PRODUTO, CAMPANHA, VALOR)
values (:itens.produto, :descontos.campanha, :descontos.valor);
#end;
#end;
Cada #SetImportJsonArray(:descontos, ...)substitui o conteúdo anterior de
:descontos — a cada volta do #For externo, o alias interno é resetado
antes de ser populado com o array aninhado daquela linha, então nunca mistura descontos de um item
com os de outro.
Exemplo completo — filtrando o retorno pro formato esperado
É aqui que o #SetImportJsonArray mostra valor de verdade: um módulo Node que devolve
vários arrays de uma vez, cada um com campos internos a mais que não interessam
pro cliente da API — e o script filtra cada um pro formato exato que o endpoint promete no
Result. Suponha esse retorno do módulo, em :RESP:
Os campos internos (ativo_sistema_legado, flag_interno,
cod_legado_erp) nunca chegam no cliente — o #SetFormatJson(:alias.campo1,
:alias.campo2) descarta tudo que não foi listado, sem precisar de nenhum
if/loop manual campo a campo.
Referência — trecho de um pedido da Shopify
Versão resumida (dados de exemplo, sem informação real) da parte do JSON de um pedido usada nos
dois exemplos abaixo — o pedido real tem muito mais campos, mas só esses importam aqui:
Um cenário comum: o webhook da Shopify grava o JSON inteiro do pedido numa coluna do banco (uma
prática comum, guardar o payload cru pra processar depois), junto com um campo
IMPORTADO que começa em 'F'. O endpoint percorre todos os pedidos
ainda não importados e, pra cada um, grava o cabeçalho, captura a
PK gerada (ver insert / #RETURN ( )), usa essa PK pra
ligar cada item ao pedido — o mesmo padrão de tabela principal + Sub-Table que já
vimos em Endpoint, só que a fonte dos dados é um JSON externo em vez do body
da própria API — e, no fechamento, marca o pedido como importado (IMPORTADO = 'T'):
select:pedido ID, NAME, corpo_json from SHOPIFY_PEDIDOS_RECEBIDOS where importado = 'F';
#for :pedido
do
#begin
-- dois arrays no primeiro nivel DESTE pedido, achados direto
#SetImportJsonArray(:itens, :pedido.corpo_json, 'line_items');
#SetImportJsonArray(:frete, :pedido.corpo_json, 'shipping_lines');
insert:newpedido into pedido_venda (cod_pedidov, name, transportadora, Valor_frete)
values( :pedido.id, :pedido.name, :frete.title, :frete.price)
#return(id_pedidov);
#for :itens
do
#begin
#SetImportJsonArray(:pagamento, :itens.price_set, 'presentment_money');
insert into PEDIDO_ITENS (PRODUTO, SKU, QUANTIDADE, VALOR, MOEDA, id_pedidov)
values (:itens.title, :itens.sku, :itens.quantity, :pagamento.amount, :pagamento.currency_code, :newpedido.id_pedidov);
#end;
-- fechamento: este pedido ja foi importado
update SHOPIFY_PEDIDOS_RECEBIDOS set importado = 'T' where ID = :pedido.id;
#end;
O select:pedido traz só os pedidos com importado = 'F', e o
#For :pedido externo percorre cada um. Há dois laços, um dentro do
outro: o de fora, um pedido por volta, e o de dentro, os itens daquele pedido.
Como corpo_json muda a cada pedido, os #SetImportJsonArray(:itens, ...) e
#SetImportJsonArray(:frete, ...) ficam dentro do laço externo — a cada
volta eles são refeitos sobre o JSON do pedido da vez (o alias é sempre substituído, nunca acumula
linhas de um pedido em cima do outro). line_items e shipping_lines já estão
no primeiro nível do pedido, então uma chamada resolve cada um. Já presentment_money só
existe dentro de cada item — por isso o #SetImportJsonArray(:pagamento, ...) fica
dentro do #For :itens do, buscando de novo a cada volta, sempre no
price_set do item da vez, nunca misturando com o de outro item. O
#return(id_pedidov) no insert do cabeçalho segue a mesma lógica do
:Main.PK que já vimos nos exemplos de Endpoint — o valor gerado é
reaproveitado como FK em cada linha de PEDIDO_ITENS.
O update no final do laço externo marca aquele pedido como importado
(importado = 'T'), usando o ID do pedido da vez. Como ele roda no mesmo
script (mesma transação) dos insert, um pedido só fica marcado como importado se o
cabeçalho e os itens dele foram gravados — e na próxima execução o
where importado = 'F' já não traz mais esse pedido. Se outra plataforma (ShopPub, VTEX,
etc.) usar um nome de campo diferente pro mesmo dado, só o ArrayName muda — o resto do
script continua igual.
Exemplo — descendo nível a nível até uma chave repetida
Já presentment_money não é tão direto — no mesmo pedido, ele aparece dentro de
total_price_set, de subtotal_price_set, de cada
line_items[].price_set, e vários outros blocos. Pra pegar especificamente o de
total_price_set (o total do pedido, sem ambiguidade com os outros), desce um nível de
cada vez, guardando cada um em memória antes de buscar no próximo:
select:pedido corpo_json from SHOPIFY_PEDIDOS_RECEBIDOS where ID = :ID_PEDIDO;
-- :pedido.corpo_json = JSON inteiro do pedido, cru
-- Nivel 1: acha "total_price_set" direto no pedido (primeiro nivel) e guarda em :total
#SetImportJsonArray(:total, :pedido.corpo_json, 'total_price_set');
-- :total agora tem os campos DESSE bloco: shop_money, presentment_money
-- Nivel 2: dentro do que acabou de ser guardado em :total (nao do pedido inteiro
-- de novo), acha "presentment_money" - so' existe UM nesse nivel, sem ambiguidade.
-- :total e' um ALIAS, nao texto - precisa passar por #SetFormatJson antes de virar
-- :fonte da proxima chamada.
#SetImportJsonArray(:total_presentment, #SetFormatJson(:total), 'presentment_money');
-- :total_presentment.amount / :total_presentment.currency_code ja sao legiveis
#SetVarResult(valor_total = :total_presentment.amount);
#SetVarResult(moeda_total = :total_presentment.currency_code);
-- Mesmo caminho de dois niveis, mas a partir de "subtotal_price_set" -
-- MESMO nome de chave (presentment_money) no nivel 2, resultado diferente,
-- porque a busca comecou de um alias diferente no nivel 1
#SetImportJsonArray(:subtotal, :pedido.corpo_json, 'subtotal_price_set');
#SetImportJsonArray(:subtotal_presentment, #SetFormatJson(:subtotal), 'presentment_money');
#SetVarResult(valor_subtotal = :subtotal_presentment.amount);
Cada chamada só olha o nível que acabou de ser guardado no alias anterior — nunca o JSON inteiro
de novo. Por isso o segundo bloco (subtotal) não corre risco de pegar o
presentment_money do total por engano: ele nunca olhou o JSON inteiro, só o pedaço
que já tinha isolado em :subtotal. É mais verboso que uma busca automática, mas
nunca deixa dúvida sobre qual das dezenas de presentment_money do pedido você está
lendo.
#SetISO8601ToDateTime ( )
Script Suportado: Somente Endpoint
O ISO 8601 é o formato padrão do sistema: toda data que o endpoint devolve já sai
nele, sem macro nenhum. O macro existe só para o caminho de ida, ler a data que chega
do JSON como texto solto.
Do JSON para o banco — #SetISO8601ToDateTime ( )
Converte uma data em texto — o valor de um campo de um JSON de integração — para uma
data de verdade, no formato que o banco aceita. É a leitura natural de um campo de data que chega por
#SetImportJsonArray ( ) ou pelo retorno de um
#CallNodeJS: nesses casos o valor entra no script como texto
solto, com T, fração de segundo ou fuso, e precisa virar uma data na hora de
gravar ou comparar. (Parâmetros declarados do endpoint como data já são convertidos automaticamente —
ver Formato de Datas.)
Para que serve
JSON de plataformas e APIs de e-commerce, como o created_at de um pedido da Shopify.
A resposta de um módulo Node.js (#CallNodeJS) que devolve datas.
valor: o texto da data — :PARAMETRO, :alias.campo, outro
macro de formatação ou #ParamTokenValue ( ) —
nunca um literal solto.
Formatos aceitos
Os formatos de data mais comuns em JSON de plataformas e integrações, todos reconhecidos sem nenhuma
configuração:
Formato
Exemplo no JSON
Só data
2021-10-01
Data e hora
2020-02-25T10:07:57
Com fração de segundo (qualquer quantidade de dígitos)
2022-06-30T12:38:16.685615
Com espaço no lugar do T
2022-06-20 12:21:53
Com fuso (-03:00, +00:00 ou Z)
2026-07-27T10:30:00-03:00
A fração de segundo é descartada, e o fuso é aceito mas o deslocamento é
ignorado: a data e a hora são usadas exatamente como vieram, sem nenhuma conversão de fuso.
Retorno
A data normalizada, entre aspas, pronta para uma coluna de data ou para uma
comparação: 'YYYY-MM-DD' quando só há data, ou 'YYYY-MM-DD HH:MM:SS' quando
há hora (com espaço entre a data e a hora, o literal que todos os bancos suportados aceitam). Se o valor de origem estiver vazio ou nulo, o resultado é NULL,
sem erro. Se o texto não for uma data válida em um dos formatos acima, a chamada é rejeitada com
HTTP 400.
Exemplo — importação de pedidos
Cada pedido do JSON traz a data de emissão como texto; o macro a converte na hora de gravar:
#SetImportJsonArray(:pedidos, :RESP.pedidos, null);
#for :pedidos
do
#begin
insert into PEDIDO_VENDA (COD_PEDIDOV, DATA_EMISSAO)
values (:pedidos.id, #SetISO8601ToDateTime(:pedidos.created_at));
#end;
Para texto fora desses formatos (por exemplo 27/07/2026), use
#SetStrToDateTime ( ).
Do banco para o JSON — não precisa de macro
Para devolver uma data no ISO 8601 não existe macro: ele é o padrão. Um campo de
Result do tipo DATE ou DATE TIME sai em ISO 8601 sozinho, e o mesmo
vale para a data de um alias.
-- campo de Result declarado como DATE TIME
-- "criado_em": "2026-07-27T12:00:00"
Se o outro sistema exigir o sufixo de fuso (-03:00 ou Z),
escreva a data com #SetFormatDateTime ( ) e declare o campo
de Result como STRING:
Um mesmo formato nos dois sentidos: #SetUnixToDateTime ( )lê a data que chega do JSON para o banco, e #SetDateTimeToUnix ( )escreve a data do banco para o JSON que você devolve ou envia.
Do JSON para o banco — #SetUnixToDateTime ( )
Converte uma data que chega como número — o timestamp Unix, a
quantidade de tempo desde 1970-01-01 00:00:00 UTC — para uma data de verdade, no formato
que o banco aceita. É o equivalente do UnixToDateTime do Delphi, e o formato de data de
muitas plataformas e linguagens (JavaScript, Java, Python, Go), que mandam a data em um JSON como
1604998869000 em vez de um texto. Para datas em texto, use
#SetISO8601ToDateTime ( ); para os outros formatos de data
em JSON, ver Datas em JSON.
Para que serve
APIs e JSON que mandam a data como número — o formato nativo de JavaScript, Java, Python e Go.
Datas de tokens JWT (exp, iat) e de eventos de pagamento e de mensageria, que mandam o horário como número.
valor: o número, inteiro ou com decimais — :PARAMETRO,
:alias.campo, outro macro de formatação ou
#ParamTokenValue ( ) — nunca um literal solto. Pode vir como número ou
como texto no JSON (1604998869000 ou "1604998869000").
fuso (opcional): o fuso em que o seu banco guarda a hora, como
'-03:00' ou '+05:30'. Sem ele, a data sai em UTC. Com ele,
o macro converte o instante UTC para a hora do relógio daquele fuso.
A unidade é reconhecida pelo tamanho do número
O mesmo macro aceita as unidades mais comuns, porque cada uma tem uma ordem de grandeza bem diferente
para datas reais:
Unidade
Exemplo
Segundos (até 10 dígitos; decimais permitidos)
1604998869
Milissegundos (12 a 13 dígitos)
1604998869000
Microssegundos (15 a 16 dígitos)
1604998869000000
Nanossegundos (18 a 19 dígitos)
1604998869000000000
A parte menor que um segundo é descartada. Sem o argumento
fuso, o resultado é em UTC, sem conversão para a hora local do
servidor — o mesmo instante dá o mesmo texto em qualquer máquina.
Retorno
A data e a hora, entre aspas, no literal 'YYYY-MM-DD HH:MM:SS' aceito por
todos os bancos suportados, pronta para uma coluna de data e hora ou para uma comparação. Se o valor de
origem estiver vazio ou nulo, o resultado é NULL, sem erro. Se não for um
número válido, a chamada é rejeitada com HTTP 400.
Exemplo
#SetImportJsonArray(:pedidos, :RESP.pedidos, null);
#for :pedidos
do
#begin
insert into PEDIDO_VENDA (COD_PEDIDOV, DATA_EMISSAO)
values (:pedidos.id, #SetUnixToDateTime(:pedidos.created));
#end;
Do banco para o JSON — #SetDateTimeToUnix ( )
Escreve uma data como timestamp Unix — a quantidade de tempo desde
1970-01-01 00:00:00 UTC —, o formato que muitas plataformas e linguagens (JavaScript, Java,
Python, Go) esperam receber.
Para que serve
Devolver a data a um cliente ou a um sistema externo que espera o número, como um front-end em JavaScript.
valor: a data a escrever, em ISO 8601 (T ou espaço entre a data e a
hora) — é como toda data de uma coluna ou de um alias chega ao script. :PARAMETRO,
:alias.campo ou outro macro de formatação; nunca um literal
solto. Para uma data em texto de outro formato, converta antes com
#SetStrToDateTime ( ).
unidade (opcional): 's' segundos (padrão), 'ms'
milissegundos, 'us' microssegundos ou 'ns' nanossegundos. A data do script
não tem fração de segundo, então as unidades menores terminam em zeros.
fuso (opcional): o fuso em que o seu banco guarda a hora, como
'-03:00' ou '+05:30'. Sem ele, a hora do banco é tratada como
UTC. Com ele, o macro converte a hora do relógio para o instante em UTC antes de
calcular o valor.
Retorno
Um número inteiro: para 2026-07-27 12:00:00, 1785153600 em
segundos ou 1785153600000 em milissegundos. Se o valor de origem estiver vazio ou nulo, o resultado é nulo, sem erro. Se não for uma
data válida, a chamada é rejeitada com HTTP 400.
Em 'ns' o número passa dos 16 dígitos, mais do que um número de
JavaScript guarda com exatidão. Se o consumidor for JavaScript, declare o campo como STRING.
Quem define como o valor sai no JSON é o tipo do campo de Result, não o macro. Declare o campo como BIGINT, para que o valor saia como número, sem aspas. O
INTEGER serve só para segundos, e apenas até 2038; os milissegundos já passam do seu limite.
Datas em JSON
Cada plataforma escreve a data de um jeito: texto em ISO 8601, um número Unix, um serial de planilha, um
formato proprietário. O ISO 8601 é o padrão do sistema: toda data que entra e sai por
uma coluna ou por um campo declarado já usa esse formato, sem macro nenhum. Os macros abaixo são para
as pontas em que o outro lado fala um formato diferente.
Em vez de um único macro que tenta adivinhar o formato, existe um par de macros para cada
formato, um para cada sentido, e o nome diz exatamente o que acontece:
Set<Formato>ToDateTimelê o valor que chegou como texto solto e o
converte em uma data de verdade para o banco; SetDateTimeTo<Formato>escreve a data do banco no formato que o outro programa vai consumir. O ISO 8601 é a exceção:
como é o padrão do sistema, só tem o macro de leitura. Um valor fora do formato esperado é rejeitado,
nunca interpretado de forma errada.
Formato
Do JSON para o banco
Do banco para o JSON
Exemplo
Texto ISO 8601 (data, data e hora, com T ou espaço, com fração, com fuso)
O banco guarda a data e a hora sem fuso, e o motor nunca converte fuso por
conta própria. Os formatos se dividem em dois grupos:
ISO 8601, número serial e ticks carregam a hora do relógio como ela está: nada é
deslocado. No ISO, um sufixo de fuso lido é ignorado.
Unix, data JSON da Microsoft e FILETIME são um instante em UTC.
Por padrão a hora do banco é tratada como UTC, nos dois sentidos. Se o seu banco guarda a hora de um
fuso, informe-o no argumento opcional fuso ('-03:00') do macro, e a
conversão passa a ser feita.
Ler e escrever com o mesmo fuso devolve exatamente o valor original.
Como a data sai no Result
Quem define como o valor aparece no JSON é o tipo do campo de Result, não o macro. Um
campo DATE ou DATE TIME sai sempre em ISO 8601, sem macro. Para
devolver outro formato, declare o campo como STRING ou TEXT (ISO com
sufixo de fuso por #SetFormatDateTime ( ), data JSON da
Microsoft), BIGINT (Unix, ticks, FILETIME) ou
DOUBLE PRECISION (número serial) e preencha com
#SetVarResult ( ) e o macro de escrita.
O que todos têm em comum
Os macros de leitura devolvem a data e a hora entre aspas, no literal
'YYYY-MM-DD HH:MM:SS' aceito por todos os bancos suportados, e descartam o que for menor que
um segundo. Os de escrita devolvem texto ou número, conforme o formato. Todos devolvem
nulo para valor vazio ou nulo e rejeitam com HTTP 400 um valor que
não esteja no formato esperado. Funcionam dentro de um #For ( ) sobre um alias
vindo de #SetImportJsonArray ( ) ou do retorno de um
#CallNodeJS.
Um mesmo formato nos dois sentidos: #SetOADateToDateTime ( )lê a data que chega do JSON para o banco, e #SetDateTimeToOADate ( )escreve a data do banco para o JSON que você devolve ou envia.
Do JSON para o banco — #SetOADateToDateTime ( )
Converte uma data que chega como número serial — a quantidade de dias
desde 1899-12-30, com a hora na parte decimal — para uma data de verdade. É o formato do
TDateTime do Delphi, do Excel (sistema de datas de 1900) e
do OADate do .NET, comum em planilhas e em sistemas que serializam datas por conta própria.
Para que serve
Planilhas Excel e arquivos exportados que trazem a data como número serial.
Sistemas Delphi que serializam o TDateTime como número.
A data e a hora, entre aspas, no literal 'YYYY-MM-DD HH:MM:SS' aceito por
todos os bancos suportados, com a hora que está no próprio número, sem nenhuma conversão de fuso. A parte menor que um segundo é descartada.
Se o valor de origem estiver vazio ou nulo, o resultado é NULL, sem erro.
Se o valor não estiver no formato esperado, a chamada é rejeitada com
HTTP 400.
Exemplo
#SetImportJsonArray(:pedidos, :RESP.pedidos, null);
#for :pedidos
do
#begin
insert into PEDIDO_VENDA (COD_PEDIDOV, DATA_EMISSAO)
values (:pedidos.id, #SetOADateToDateTime(:pedidos.data));
#end;
Do banco para o JSON — #SetDateTimeToOADate ( )
Escreve uma data como número serial — a quantidade de
dias desde 1899-12-30, com a hora na parte decimal. É o formato do
TDateTime do Delphi, do Excel e do OADate
do .NET.
Para que serve
Devolver a data a um cliente Delphi ou a uma planilha que espera o número serial.
#SetDateTimeToOADate(:alias.data)
Argumentos
valor: a data a escrever, em ISO 8601 (T ou espaço entre a data e a
hora) — é como toda data de uma coluna ou de um alias chega ao script. :PARAMETRO,
:alias.campo ou outro macro de formatação; nunca um literal
solto. Para uma data em texto de outro formato, converta antes com
#SetStrToDateTime ( ).
Não tem argumento de fuso: o número carrega a hora do relógio como ela está, sem
nenhuma conversão.
Retorno
Um número com decimais: para 2026-07-27 12:00:00, 46230.5.
Se o valor de origem estiver vazio ou nulo, o resultado é nulo, sem erro. Se não for uma
data válida, a chamada é rejeitada com HTTP 400.
A hora está na parte decimal, e a maioria das horas gera uma dízima (14:30 é
0,6041666…). Evite um DECIMAL com poucas casas — ele
arredondaria o número e deslocaria a hora; o DOUBLE PRECISION preserva as casas.
Quem define como o valor sai no JSON é o tipo do campo de Result, não o macro. Declare o campo como DOUBLE PRECISION, para que o valor saia como número, sem aspas, e
mantenha as casas decimais da hora.
Um mesmo formato nos dois sentidos: #SetMsJsonToDateTime ( )lê a data que chega do JSON para o banco, e #SetDateTimeToMsJson ( )escreve a data do banco para o JSON que você devolve ou envia.
Do JSON para o banco — #SetMsJsonToDateTime ( )
Converte a data JSON da Microsoft, em texto no formato /Date(...)/, para uma
data de verdade. É o formato de datas serializadas por APIs ASP.NET e WCF antigas: o
número entre parênteses são os milissegundos desde 1970-01-01 UTC, e um
sufixo de fuso opcional (-0300) é aceito e ignorado — os milissegundos já
são UTC. Quem desloca a hora é o argumento fuso.
Para que serve
Serviços ASP.NET e WCF antigos, que mandam a data como /Date(...)/.
fuso (opcional): o fuso em que o seu banco guarda a hora, como
'-03:00' ou '+05:30'. Sem ele, a data sai em UTC. Com ele,
o macro converte o instante UTC para a hora do relógio daquele fuso.
Formato reconhecido
Descrição
No JSON
Sem fuso
/Date(1604998869000)/
Com fuso (ignorado)
/Date(1604998869000-0300)/
Retorno
A data e a hora, entre aspas, no literal 'YYYY-MM-DD HH:MM:SS' aceito por
todos os bancos suportados, em UTC (ou no fuso informado). A parte menor que um segundo é descartada.
Se o valor de origem estiver vazio ou nulo, o resultado é NULL, sem erro.
Se o valor não estiver no formato esperado, a chamada é rejeitada com
HTTP 400.
Exemplo
#SetImportJsonArray(:pedidos, :RESP.pedidos, null);
#for :pedidos
do
#begin
insert into PEDIDO_VENDA (COD_PEDIDOV, DATA_EMISSAO)
values (:pedidos.id, #SetMsJsonToDateTime(:pedidos.data));
#end;
Do banco para o JSON — #SetDateTimeToMsJson ( )
Escreve uma data como a data JSON da Microsoft, em texto no formato
/Date(...)/, o que APIs ASP.NET e WCF antigas esperam receber. O número
entre parênteses são os milissegundos desde 1970-01-01 UTC.
Para que serve
Responder a um cliente .NET antigo, que só entende esse formato.
valor: a data a escrever, em ISO 8601 (T ou espaço entre a data e a
hora) — é como toda data de uma coluna ou de um alias chega ao script. :PARAMETRO,
:alias.campo ou outro macro de formatação; nunca um literal
solto. Para uma data em texto de outro formato, converta antes com
#SetStrToDateTime ( ).
fuso (opcional): o fuso em que o seu banco guarda a hora, como
'-03:00' ou '+05:30'. Sem ele, a hora do banco é tratada como
UTC. Com ele, o macro converte a hora do relógio para o instante em UTC antes de
calcular os milissegundos, e acrescenta o sufixo (-0300) ao texto.
Retorno
Texto: para 2026-07-27 12:00:00, /Date(1785153600000)/. Com o fuso informado,
/Date(1785164400000-0300)/. Se o valor de origem estiver vazio ou nulo, o resultado é nulo, sem erro. Se não for uma
data válida, a chamada é rejeitada com HTTP 400.
Quem define como o valor sai no JSON é o tipo do campo de Result, não o macro. Declare o
campo como STRING ou TEXT; um campo DATE ou
DATE TIME só aceita data em ISO 8601 e rejeita este valor com erro de configuração
(HTTP 500).
#SetTicksToDateTime ( ) / #SetDateTimeToTicks ( )
Script Suportado: Somente Endpoint
Um mesmo formato nos dois sentidos: #SetTicksToDateTime ( )lê a data que chega do JSON para o banco, e #SetDateTimeToTicks ( )escreve a data do banco para o JSON que você devolve ou envia.
Do JSON para o banco — #SetTicksToDateTime ( )
Converte os ticks do .NET — a quantidade de intervalos de 100
nanossegundos desde 0001-01-01 (DateTime.Ticks) — para uma data de
verdade. Aparece em integrações com sistemas .NET que serializam a data como um número inteiro grande,
de 18 dígitos.
Para que serve
Integrações com sistemas .NET que serializam o DateTime.Ticks como número inteiro.
Colunas BIGINT de bancos que guardam a data como ticks.
A data e a hora, entre aspas, no literal 'YYYY-MM-DD HH:MM:SS' aceito por
todos os bancos suportados, com a hora que está no próprio número, sem nenhuma conversão de fuso. A parte menor que um segundo é descartada.
Se o valor de origem estiver vazio ou nulo, o resultado é NULL, sem erro.
Se o valor não estiver no formato esperado, a chamada é rejeitada com
HTTP 400.
Exemplo
#SetImportJsonArray(:pedidos, :RESP.pedidos, null);
#for :pedidos
do
#begin
insert into PEDIDO_VENDA (COD_PEDIDOV, DATA_EMISSAO)
values (:pedidos.id, #SetTicksToDateTime(:pedidos.data));
#end;
Do banco para o JSON — #SetDateTimeToTicks ( )
Escreve uma data como os ticks do .NET — a quantidade de intervalos de
100 nanossegundos desde 0001-01-01 (DateTime.Ticks), um
número inteiro de 18 dígitos.
Para que serve
Devolver a data a um sistema .NET que espera os ticks.
#SetDateTimeToTicks(:alias.data)
Argumentos
valor: a data a escrever, em ISO 8601 (T ou espaço entre a data e a
hora) — é como toda data de uma coluna ou de um alias chega ao script. :PARAMETRO,
:alias.campo ou outro macro de formatação; nunca um literal
solto. Para uma data em texto de outro formato, converta antes com
#SetStrToDateTime ( ).
Não tem argumento de fuso: o número carrega a hora do relógio como ela está, sem
nenhuma conversão.
Retorno
Um número inteiro: para 2026-07-27 12:00:00,
639207504000000000. Se o valor de origem estiver vazio ou nulo, o resultado é nulo, sem erro. Se não for uma
data válida, a chamada é rejeitada com HTTP 400.
Com 18 dígitos, o número passa do que um número de JavaScript guarda com exatidão. Se
o consumidor for JavaScript, declare o campo como STRING.
Quem define como o valor sai no JSON é o tipo do campo de Result, não o macro. Declare o campo como BIGINT, para que o valor saia como número, sem aspas.
Um mesmo formato nos dois sentidos: #SetFileTimeToDateTime ( )lê a data que chega do JSON para o banco, e #SetDateTimeToFileTime ( )escreve a data do banco para o JSON que você devolve ou envia.
Do JSON para o banco — #SetFileTimeToDateTime ( )
Converte um FILETIME do Windows — a quantidade de intervalos de 100
nanossegundos desde 1601-01-01 — para uma data de verdade. É o formato de datas do
Active Directory/LDAP e de várias APIs do Windows, também um número inteiro de cerca de
18 dígitos.
Para que serve
Consultas ao Active Directory/LDAP, cujos atributos de data (lastLogon, pwdLastSet, accountExpires) são FILETIME.
APIs do Windows que devolvem a data nesse formato.
fuso (opcional): o fuso em que o seu banco guarda a hora, como
'-03:00' ou '+05:30'. Sem ele, a data sai em UTC. Com ele,
o macro converte o instante UTC para a hora do relógio daquele fuso.
Formato reconhecido
Descrição
No JSON
FILETIME
132494724690000000
Retorno
A data e a hora, entre aspas, no literal 'YYYY-MM-DD HH:MM:SS' aceito por
todos os bancos suportados, em UTC (ou no fuso informado). A parte menor que um segundo é descartada.
Se o valor de origem estiver vazio ou nulo, o resultado é NULL, sem erro.
Se o valor não estiver no formato esperado, a chamada é rejeitada com
HTTP 400.
Exemplo
#SetImportJsonArray(:pedidos, :RESP.pedidos, null);
#for :pedidos
do
#begin
insert into PEDIDO_VENDA (COD_PEDIDOV, DATA_EMISSAO)
values (:pedidos.id, #SetFileTimeToDateTime(:pedidos.data));
#end;
Do banco para o JSON — #SetDateTimeToFileTime ( )
Escreve uma data como um FILETIME do Windows — a quantidade de
intervalos de 100 nanossegundos desde 1601-01-01 UTC —, o formato do
Active Directory/LDAP e de várias APIs do Windows.
Para que serve
Devolver a data a um sistema Windows ou ao Active Directory que espera FILETIME.
valor: a data a escrever, em ISO 8601 (T ou espaço entre a data e a
hora) — é como toda data de uma coluna ou de um alias chega ao script. :PARAMETRO,
:alias.campo ou outro macro de formatação; nunca um literal
solto. Para uma data em texto de outro formato, converta antes com
#SetStrToDateTime ( ).
fuso (opcional): o fuso em que o seu banco guarda a hora, como
'-03:00' ou '+05:30'. Sem ele, a hora do banco é tratada como
UTC. Com ele, o macro converte a hora do relógio para o instante em UTC antes de
calcular o valor.
Retorno
Um número inteiro: para 2026-07-27 12:00:00,
134296272000000000. Se o valor de origem estiver vazio ou nulo, o resultado é nulo, sem erro. Se não for uma
data válida, a chamada é rejeitada com HTTP 400.
Com 18 dígitos, o número passa do que um número de JavaScript guarda com exatidão. Se
o consumidor for JavaScript, declare o campo como STRING.
Quem define como o valor sai no JSON é o tipo do campo de Result, não o macro. Declare o campo como BIGINT, para que o valor saia como número, sem aspas.
Macros XML
Muitas integrações ainda respondem em XML — serviços SOAP, notas fiscais de serviço,
sistemas mais antigos. O envio em XML já é feito pelo
#CallHTTP, com BodyPayload: Text 'text/xml' { ... }. Para
ler um XML dentro do script, use
#SetImportXml ( ): ele transforma o XML num
alias novo em memória, do mesmo jeito que o
#SetImportJsonArray ( ) faz com JSON.
O XML pode vir de qualquer lugar: o retorno de um #CallHTTP
(:alias), o retorno de um #CallNodeJS ou
#CallDLL, um parâmetro do endpoint ou uma coluna do banco.
#SetImportXml ( )
Script Suportado: Somente Endpoint
Importa um bloco de um texto XML pra dentro de um alias novo em memória,
iterável por #For como qualquer outro alias. Funciona do mesmo jeito que o
#SetImportJsonArray ( ): você diz o nome do bloco
que quer, cada ocorrência dele vira uma linha, e os blocos que estão dentro dele continuam guardados
num campo — prontos pra serem importados de novo, um nível de cada vez. Não precisa de nenhuma
declaração prévia no endpoint — o formato só é conhecido em tempo de execução.
Importante: #SetImportXml nunca escreve no Result sozinho — ele só
materializa o XML num alias em memória. O Result é montado depois, com
#SetVarResult ( ), select ou
#SetFormatJson ( ) por cima do alias que ele acabou de criar.
#SetImportXml(:alias_novo, :fonte, 'Bloco');
Os três argumentos são sempre obrigatórios — a mesma assinatura fixa do
#SetImportJsonArray:
:alias_novo — nome do alias que passa a existir a partir dessa linha, usado
exatamente como qualquer alias multi-linha.
:fonte — o texto XML de origem. Pode vir de qualquer lugar: o retorno de um
#CallHTTP (:alias), o retorno de um
#CallNodeJS ou #CallDLL, um parâmetro
do endpoint, uma coluna do banco ou um campo de outro alias criado por um
#SetImportXml anterior (ex: :det.prod).
'Bloco' — o nome da tag do bloco cujas ocorrências viram linhas,
sem caminho (ex: 'det'), ou a palavra-chave null quando a
própria :fonte já é o bloco que se quer importar (uma linha só, com o elemento principal).
Ex.: #SetImportXml(:prod, :det.prod, null);
Cada chamada substitui o conteúdo anterior do alias — se #SetImportXml
for chamado de novo pro mesmo nome (ex: dentro de um #For, uma vez por volta), o alias é
resetado antes de ser populado, nunca acumula linha de uma chamada em cima da outra.
Como o bloco é encontrado
A tag informada no 3º argumento é procurada em qualquer profundidade da
:fonte — não é preciso escrever os níveis de cima. Cada ocorrência dessa
tag vira uma linha do alias: numa NF-e com dois itens, 'det' gera duas linhas.
-- uma linha por item da nota (as duas tags <det>)
#SetImportXml(:det, :doc.XML_NOTA, 'det');
-- o bloco ja' esta' pronto no campo: uma linha so'
#SetImportXml(:prod, :det.prod, null);
Se a tag não existir na :fonte, o resultado é um alias vazio — nunca erro (um XML externo
pode legitimamente não trazer aquele bloco numa chamada específica), igual ao
#SetImportJsonArray.
Como o XML vira campos
Tag simples (só com texto) vira campo com o nome da tag:
<xPed>00012345</xPed> vira :alias.xPed com o valor
00012345. O conteúdo de CDATA entra como texto normal.
Atributos do bloco também viram campos: <det nItem="1"> gera
:det.nItem.
Bloco dentro do bloco (tag com outras tags dentro) vira um campo que guarda o
XML completo daquele bloco, com a tag de abertura e a de fechamento — por exemplo,
:det.prod guarda <prod>...</prod> inteiro. Assim ele pode ser
importado de novo a partir do campo, um nível de cada vez, do mesmo jeito que o
#SetImportJsonArray faz com um JSON dentro de outro.
Os dados de fora do bloco não são repetidos em cada linha: o cabeçalho da nota
(ide, emit, totais) vem da sua própria chamada.
Prefixos de namespace são ignorados: ns2:Numero é lido como Numero.
Não é preciso escrever prefixo nenhum no nome do bloco.
Em uma resposta SOAP, o Envelope e o Body são pulados
automaticamente.
Todo valor chega como texto; quem define o formato de saída é o tipo do campo no
Result, igual ao JSON.
Tags com o mesmo nome em lugares diferentes
Um XML fiscal repete o mesmo nome de tag em blocos diferentes — numa NF-e, CNPJ aparece
no emitente e no destinatário; vProd aparece em cada item e no total. Para não misturar,
importe o bloco pai e leia o campo dele: 'emit' gera
:emit.CNPJ (o vendedor) e 'dest' gera :dest.CNPJ (o comprador);
:prod.vProd é o valor do item e :tot.vProd, o da nota.
Referência — trecho de uma NF-e de venda
Os exemplos abaixo usam este XML (dados fictícios). Cada produto traz o pedido de compra do cliente em
xPed e o item desse pedido em nItemPed; os impostos já estão incluídos no
preço:
Exemplo — itens da NF-e com o pedido do comprador e o ICMS
O XML da nota está gravado numa coluna. O cabeçalho vem de uma chamada para cada bloco; os itens vêm
de 'det', e dentro do #For cada item abre o próprio prod e os
próprios blocos de imposto (ICMS00, PISAliq e COFINSAliq) — o
imposto lido é sempre o daquele item:
O nome do bloco é exato. O grupo de imposto muda de nome conforme o CST — numa nota
com outro CST, declare o bloco correspondente (ex.: 'ICMS20', 'ICMS40',
'ICMSSN102'), uma importação por nome. Um bloco que não existe no item gera um alias
vazio, sem erro.
Exemplo — consulta de NFS-e (SOAP)
O envio é feito pelo #CallHTTP com o corpo em XML; a resposta inteira
fica em :NFSE, e o #SetImportXml lê dela só o que interessa.
O Envelope/Body do SOAP é pulado e basta o nome do bloco:
:fonte que não é um XML válido interrompe o script com
HTTP 500, igual a um JSON inválido no
#SetImportJsonArray.
Bloco que não existe, ou :fonte vazio, resulta em alias vazio.
O 3º argumento é só o nome do bloco: um caminho com / (ex.:
'NFe/infNFe/det') interrompe o script com HTTP 500.
Macros de Formatação
Controlam como um valor é representado antes de entrar no comando — arredondamento,
truncamento, conversão de tipo, forçar aspas de texto, ou ler e escrever datas. Importam sempre que o resultado vem de uma
conta feita dentro do próprio SQL (soma, multiplicação) ou de um campo de
alias sem tipo declarado, e precisa virar um número "redondo" — com uma quantidade
certa de casas decimais — antes de ser gravado ou comparado. Quando o destino é uma coluna de
texto, é #QuotedStr ( ) quem força as aspas duplas.
A origem do valor só pode ser um parâmetro declarado no endpoint
(:PARAMETRO), um alias em memória (:alias.campo), a
composição de uma macro deste grupo com outra, ou #ParamTokenValue ( ) —
nunca um valor literal solto (se o número já é fixo, escreve direto no comando, sem macro nenhuma).
Fazem parte deste grupo:
#QuotedStr ( ) — força um valor a sair entre
aspas (texto), quando o motor não consegue decidir sozinho.
#SetToFloat ( ) — converte um valor pra
número de ponto flutuante, sem arredondar nem truncar.
#SetToRound ( ) — arredonda um valor pra
uma quantidade de casas decimais (arredondamento comercial).
#SetToTrunc ( ) — corta um valor na casa
decimal pedida, sem arredondar.
#SetToInteger ( ) — descarta a parte
decimal de um valor, virando um inteiro (atalho pra #SetToTrunc com zero casas).
#SetStrToDateTime ( ) — lê uma data
em texto escrita em outro formato, informado por uma máscara (ex.:
27/07/2026 com dd/mm/yyyy).
Converte um valor recebido como texto ou inteiro num literal de
ponto flutuante — útil quando a origem do dado (um parâmetro, um campo de alias) não carrega o tipo
certo junto, e o comando precisa de um número de verdade, não de uma string que parece número. Cuida
só do tipo, não da quantidade de casas decimais — não recebe N nenhum.
insert into PRODUTO (CODIGO, PESO) values (:CODIGO, #SetToFloat(:PESO));
Arredonda um valor para a quantidade de casas decimais informada — arredondamento
comercial (a partir de 5 na casa seguinte, arredonda para cima em valor absoluto;
abaixo de 5, para baixo), o mesmo usado em cálculo de preço, desconto e totais financeiros.
Funciona também com negativos, sempre pela distância de zero:
#SetToRound(:VALOR, 2)
-- 10,565 -> 10.57
-- 10,564 -> 10.56
-- -10,5 -> -11 (arredonda afastando de zero, nao em direcao a zero)
Exemplo gravando um total já arredondado em duas casas:
update PEDIDO_VENDA set TOTAL = #SetToRound(:TOTAL, 2) where PEDIDO_VENDA = :PEDIDO_VENDA;
#SetToTrunc ( )
Script Suportado: Endpoint e ObjectArray
Corta um valor na quantidade de casas decimais informada sem arredondar — o que
vier depois da casa pedida é simplesmente descartado, para cima ou para baixo tanto faz.
Atalho para #SetToTrunc ( ) com zero casas decimais — descarta
toda a parte fracionária de um valor, sem arredondar. Para arredondar para o inteiro mais próximo em
vez de truncar, use #SetToRound(:campo, 0).
Exemplo gravando a quantidade recebida (um parâmetro) como inteiro, sem casas decimais:
insert into MOVIMENTO_ESTOQUE (PRODUTO, QUANTIDADE, TIPO)
values (:PRODUTO, #SetToInteger(:QUANTIDADE), 'ENTRADA');
E a partir de um campo de alias:
update ESTOQUE set SALDO = SALDO - #SetToInteger(:item.QUANTIDADE) where PRODUTO = :item.PRODUTO;
#SetStrToDateTime ( )
Script Suportado: Endpoint e ObjectArray
Lê uma data escrita em texto, em um formato que você informa por uma
máscara, e a converte para uma data de verdade, no formato que o banco aceita. É o
equivalente do StrToDateTime do Delphi, e é o macro para qualquer texto de
data que não esteja em um dos formatos de Datas em JSON
(ISO 8601, Unix, número serial, data JSON da Microsoft, ticks, FILETIME). Esses seis têm macro próprio
e não precisam de máscara; aqui você diz como o texto está escrito.
Uma plataforma ou um JSON que manda a data como 27/07/2026 — um JSON importado por #SetImportJsonArray ( ) ou o retorno de um #CallNodeJS.
Uma coluna VARCHAR do ERP legado, com a data escrita como texto.
Um arquivo importado (CSV, planilha, layout fixo) com a data em ddmmyyyy. Dígitos sem separador também valem: 20260727 com a máscara yyyymmdd.
Um parâmetro digitado pelo usuário em formato brasileiro.
Argumentos
1º — valor: o texto da data. Mesma regra dos demais macros de formatação:
:PARAMETRO, :alias.campo, outro macro deste grupo ou
#ParamTokenValue ( ) — nunca um literal solto.
2º — máscara (obrigatória): como o texto está escrito, entre aspas
simples. Ela diz ao macro qual parte é o dia, o mês e o ano; os separadores precisam bater com o texto.
Máscara
Só as letras abaixo são reconhecidas como partes da data; todo o resto é texto fixo. Os
mesmos símbolos valem em #SetFormatDateTime ( ).
Letras
Significado
yyyy / yy
ano com 4 ou 2 dígitos
mm
mês (01–12) — ou minuto (00–59) quando vem logo depois de hh, como no Delphi
dd
dia (01–31)
hh
hora (00–23)
nn
minuto (00–59) — igual ao mm depois de hh, e nunca é ambíguo
ss
segundo (00–59)
zzz
milissegundos (000–999) — os segundos são ss: hh:mm:ss.zzz
Retorno
A data normalizada, entre aspas, pronta para uma coluna de data ou para uma
comparação: '2026-07-27' quando a máscara só tem data, ou
'2026-07-27 14:30:00' quando tem hora (o literal aceito por todos os bancos suportados). Se o valor de origem estiver vazio ou
nulo, o resultado é NULL, sem erro. Se o texto não bater com a máscara (ou não
for uma data válida), a chamada é rejeitada com HTTP 400.
Exemplos
Importação de um JSON externo — a plataforma manda a data como
27/07/2026 e a coluna de destino é uma data:
#SetImportJsonArray(:itens, :pedido.corpo_json, 'line_items');
#for :itens
do
#begin
insert into PEDIDO_ITENS (PRODUTO, DATA_ENTREGA)
values (:itens.sku, #SetStrToDateTime(:itens.entrega, 'dd/mm/yyyy'));
#end;
Comparação com uma coluna de data, a partir de um texto em barra:
select PEDIDO, CLIENTE from PEDIDOS
where DATA_ENTREGA = #SetStrToDateTime(:DATA_TEXTO, 'dd/mm/yyyy');
Para um dos formatos de Datas em JSON, use o macro do próprio formato, que
não precisa de máscara. Para mudar de um formato de texto para outro (ler e escrever), combine com
#SetFormatDateTime ( ).
#SetFormatDateTime ( )
Script Suportado: Endpoint e ObjectArray
Escreve uma data ou data/hora na máscara pedida, devolvendo
texto. É o passo de escrita: serve quando o destino — um sistema externo, uma
coluna de texto, um relatório — exige um formato diferente do ISO 8601 usado no transporte da API (ver
Formato de Datas). O valor de origem precisa estar em ISO 8601 (ou ser o
resultado de #SetISO8601ToDateTime ( ) ou de
#SetStrToDateTime ( )).
Um só macro para data e para data/hora — é a máscara que decide se a hora aparece.
Para que serve
O outro sistema exige a data como dd/mm/yyyy, ou como ISO 8601 com o sufixo de fuso (-03:00).
Gravar em uma coluna VARCHAR do ERP legado, que guarda a data como texto.
Um campo de Result STRING, para uma tela que quer a data já formatada.
Montar texto de e-mail, de relatório ou nome de arquivo (yyyymmdd).
Montar linhas de layouts como SPED (ddmmyyyy) ou CNAB (ddmmyy).
Para escrever em um dos formatos de Datas em JSON (Unix, número serial,
data JSON da Microsoft, ticks, FILETIME), use o macro de escrita do próprio formato.
Argumentos
1º — valor: a data a escrever, em ISO 8601 (YYYY-MM-DD ou
YYYY-MM-DDTHH:MM:SS). Mesma regra dos demais macros de formatação:
:PARAMETRO, :alias.campo, outro macro deste grupo ou
#ParamTokenValue ( ) — nunca um literal solto.
2º — máscara de saída: como o texto deve ficar, entre aspas simples.
Máscara
Só as letras abaixo são substituídas por partes da data; todo o resto é texto fixo,
inclusive os separadores. Letras que devem sair como texto vão entre aspas duplas
(ex.: "T", "de").
Letras
Significado
yyyy / yy
ano com 4 ou 2 dígitos
mm
mês (01–12) — ou minuto (00–59) quando vem logo depois de hh, como no Delphi
dd
dia (01–31)
hh
hora (00–23)
nn
minuto (00–59) — igual ao mm depois de hh, e nunca é ambíguo
ss
segundo (00–59)
zzz
milissegundos (000–999) — os segundos são ss: hh:mm:ss.zzz
Os separadores / e : saem exatamente como escritos —
não dependem das configurações regionais do servidor.
Retorno
O resultado é texto, já entre aspas, pronto para entrar no SQL ou em um
#SetVarResult ( ) — não precisa de
#QuotedStr ( ). Se o valor de origem estiver vazio ou
nulo, o resultado é NULL, sem erro. Se for uma data só (sem hora) e a máscara
tiver hora, a hora sai 00:00:00. Se o valor não for uma data válida em ISO 8601, a
chamada é rejeitada com HTTP 400.
Exemplos
Enviando a data a um sistema externo que exige outro formato, e
devolvendo-a formatada na resposta:
Comparação com uma coluna de texto do ERP gravada como dd/mm/yyyy, a
partir de um parâmetro que chega em ISO:
select PEDIDO, CLIENTE from PEDIDOS_LEGADO
where DATA_TXT = #SetFormatDateTime(:DATA, 'dd/mm/yyyy');
Funciona para igualdade. Texto em dd/mm/yyyy não ordena como data, então
uma faixa (between) sobre uma coluna assim precisa ser tratada no próprio
SQL, com as funções do banco.
De um formato de texto para outro — lê com
#SetStrToDateTime ( ) (ou com
#SetISO8601ToDateTime ( ), se o texto já for ISO) e
escreve com #SetFormatDateTime, como no Delphi (StrToDateTime +
FormatDateTime):