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 DATE nunca devolve hora, minuto e segundo, mesmo que a coluna seja um timestamp; um campo DATE TIME sempre 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:

curl -u usuario:senha http://<ip>:<port>/<apiname>/api/<method_signature_path>/<endpoint>

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:

[EmitirNotasPendentes]
endpoint=/crudapi/api/notaFiscal/emitirPendentes
Active=True
ExecutionMode=Interval
IntervalMinutes=5

[RelatorioDiario]
endpoint=/crudapi/api/relatorio/fechamentoDiario
ExecutionMode=OnceDaily
DailyTime=06:00

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:

endpoint=/crudapi/api/Pedidos/Pedido_integrado/Alterar_status?PEDIDO_VENDA=1

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.

[Listar_embalagens]
endpoint=/SampleStore/api/Simulator_mix/Mix/Listar_embalagens?ID_EMPRESA=1
Active=False
ExecutionMode=Interval
IntervalMinutes=5

lastrun — controlado automaticamente

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.

[EmitirNotasPendentes]
endpoint=/crudapi/api/notaFiscal/emitirPendentes
ExecutionMode=Interval
IntervalMinutes=5
email=financeiro@empresa.com.br

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):

[RelatorioDiario]
endpoint=/crudapi/api/relatorio/fechamentoDiario
ExecutionMode=OnceDaily
DailyTime=06:00
ftphost=ftp.empresa.com.br
ftpuser=integracao
ftppassword=********
ftpdir=/relatorios

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):

# SingleThreadScheduler_Shoppub_id_cli01.ini

[Queue]
IntervalEndpointsMS=2000

[SincronizarPedidos]
endpoint=/crudapi/api/shoppub/sincronizarPedidos
ExecutionMode=Interval
IntervalMinutes=5

[SincronizarEstoque]
endpoint=/crudapi/api/shoppub/sincronizarEstoque
ExecutionMode=Interval
IntervalMinutes=10
  • 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:

C:\CrudSqlServer\NodeBridge\Modules\Fretes\calcularFrete.js
C:\CrudSqlServer\NodeBridge\Modules\NotasFiscais\emitirSefaz.js
C:\CrudSqlServer\NodeBridge\Modules\Correios\rastrearEncomenda.js

No script, Module: usa o caminho a partir de Modules\, sem o .js:

Module: "Fretes/calcularFrete"
Module: "NotasFiscais/emitirSefaz"
Module: "Correios/rastrearEncomenda"
⚠️ 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 processos node.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:

#CallNodeJS:RESP(
    Module: "consultar_cep",
    Params: { cep = :CEP },
    TimeOut: 10 {
        #SetVarResult(status = 'timeout');
        #SetResponseCode(504);
    },
    Error: {
        #SetVarResult(status = 'erro');
        #SetVarResult(motivo = #Node_Error(message));
        #SetResponseCode(504);
    },
    Success: {
        #SetVarResult(status = 'ok');
        #SetVarResult(logradouro = :RESP.logradouro);
        #SetVarResult(bairro = :RESP.bairro);
        #SetVarResult(cidade = :RESP.cidade);
        #SetVarResult(uf = :RESP.uf);
    }
);

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

Resposta esperada:

{
    "values": {
        "status": "ok",
        "logradouro": "Praça da Sé",
        "bairro": "Sé",
        "cidade": "São Paulo",
        "uf": "SP"
    }
}
💡 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:

C:\CrudSqlServer\DLLBridge\Libraries\crud_documentos.dll
C:\CrudSqlServer\DLLBridge\Libraries\ACBr\crud_acbr_nfe.dll
C:\CrudSqlServer\DLLBridge\Libraries\Bancos\crud_boleto.dll

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:

Library: "crud_documentos.ValidarCPF"
Library: "ACBr/crud_acbr_nfe.StatusServico"
Library: "Bancos/crud_boleto.Registrar"

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.

{
    "username": "...",
    "password": "...",
    "baseUrl": "http://127.0.0.1:6081",
    "libraryFolder": "C:\\CrudSqlServer\\DLLBridge\\Libraries\\ACBr"
}

Concorrência e isolamento

As DLLs não rodam dentro do CrudSqlServer. O servidor mantém um pool de processos CrudDLLHost.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:

C:\CrudSqlServer\DLLBridge\Libraries\ACBr\crud_acbr_nfe.dll
C:\CrudSqlServer\DLLBridge\Libraries\ACBr\ACBrNFe64.dll
C:\CrudSqlServer\DLLBridge\Libraries\ACBr\ACBrLib.ini
C:\CrudSqlServer\DLLBridge\Libraries\ACBr\Schemas\
#CallDLL:SEFAZ(
    Library: "ACBr/crud_acbr_nfe.StatusServico",
    Params: { uf = :UF },
    TimeOut: 30 {
        #SetVarResult(status = 'timeout');
        #SetResponseCode(504);
    },
    Error: {
        #SetVarResult(status = 'erro');
        #SetVarResult(motivo = #DLL_Error(message));
        #SetResponseCode(502);
    },
    Success: {
        #SetVarResult(status = 'ok');
        #SetVarResult(cStat = :SEFAZ.cStat);
        #SetVarResult(xMotivo = :SEFAZ.xMotivo);
    }
);

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.

1. Crie o Endpoint

  • Método: GET
  • Processing Mode: PL/SQL Script

2. Declare o Parâmetro

  • Nome: CPF
  • Tipo: STRING
  • Required: sim

3. Escreva o Script (PL/SQL Script)

#CallDLL:DOC(
    Library: "crud_documentos.ValidarCPF",
    Params: { cpf = :CPF },
    TimeOut: 5 {
        #SetVarResult(status = 'timeout');
        #SetResponseCode(504);
    },
    Error: {
        #SetVarResult(status = 'erro');
        #SetVarResult(motivo = #DLL_Error(message));
    },
    Success: {
        #SetVarResult(status = 'ok');
        #SetVarResult(motivo = '');
        #SetVarResult(valido = :DOC.valido);
    }
);

4. Declare o Result

Três campos, todos STRING: status, motivo e valido.

5. Chame o endpoint

GET /CrudApi/api/<sua_api>/<seu_endpoint>?cpf=52998224725

Resposta esperada:

{
    "values": {
        "status": "ok",
        "motivo": "",
        "valido": "true"
    }
}
💡 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.

#HttpAddUrl('/produtos/saldodeestoque');
#HttpParam('produto', :PRODUTO);
#HttpParam('vitrine', 201);
#CallHTTP:SALDO(
    TimeOut: 15 { #SetVarResult(status = 'Consulta em andamento'); },
    Error:   { #SetVarResult(motivo = #Http_Error(message)); #SetResponseCode(502); },
    Success: { #SetVarResult(saldo = :SALDO.saldo); }
);

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:

http://<ip>:<port>/<apiname>/api/<method_signature_path>/<endpoint>?AccessToken=3fa85f64-5717-4562-b3fc-2c963f66afa6

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:

#Call minhaapi.api.pedidos.criar_pedido(
    chave_empresa = #ParamTokenValue(chave_empresa),
    numero_pedido = :NUMERO_PEDIDO
);

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}

$format nã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
{
  "scanned": 39,
  "created": 37,
  "upToDate": 0,
  "failed": 1,
  "formatMismatch": 1,
  "truncated": false,
  "formatMismatchFiles": [ { "file": "jpg\\Produtos\\foto_12.jpg", "extension": ".jpg", "actualFormat": "png" } ],
  "errorFiles": [ { "file": "jpg\\Produtos\\quebrada.jpg", "error": "Input file contains unsupported image format" } ]
}

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.

Postman

POST .../Pedido_integrado/Post_img?$filename=abc123.jpg Send Params Authorization Headers Body none form-data x-www-form-urlencoded raw binary abc123.jpg image/jpeg
  • Method: POST
  • URL: https://meuservidor/ApiName/api/Pedidos/Pedido_integrado/Post_img?$filename=abc123.jpg
  • Aba Headers: AccessToken: 3fa85f64-5717-4562-b3fc-2c963f66afa6 (ou usuário/senha em Authorization → Basic Auth)
  • Aba Body → opção binary → selecionar o arquivo do disco

Python (requests) — chamada sem usuário e senha, autenticada só pelo AccessToken:

import requests

url = "https://meuservidor/ApiName/api/Pedidos/Pedido_integrado/Post_img"
params = {"$filename": "abc123.jpg"}

# nenhuma credencial de usuário/senha é enviada — só o token
headers = {"AccessToken": "3fa85f64-5717-4562-b3fc-2c963f66afa6"}

with open("abc123.jpg", "rb") as f:
    resp = requests.post(url, params=params, headers=headers, data=f)

print(resp.status_code, resp.text)

Node.js (fetch)

const fs = require("fs");

const url = "https://meuservidor/ApiName/api/Pedidos/Pedido_integrado/Post_img"
  + "?$filename=abc123.jpg";

const resp = await fetch(url, {
  method: "POST",
  headers: { "AccessToken": "3fa85f64-5717-4562-b3fc-2c963f66afa6" },
  body: fs.readFileSync("abc123.jpg"),
});

console.log(resp.status, await resp.text());

PHP (cURL)

<?php
$url = "https://meuservidor/ApiName/api/Pedidos/Pedido_integrado/Post_img"
     . "?$filename=abc123.jpg";

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, file_get_contents("abc123.jpg"));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "AccessToken: 3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "Content-Type: application/octet-stream",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), "\n", $response;

Armazenamento físico

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 ]

Ver [ ] Params Condicional para mais exemplos.

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):

insert:Main into PRODUTO ( NOME, MARCA, PESO, ATIVO )
values ( :NOME, :MARCA, :PESO, :ATIVO ) #RETURN(ID_PRODUTO);

#for :grade_produto
do
#begin
  insert into PRODUTO_SKU ( ID_PRODUTO, COR, TAMANHO, SKU, ESTOQUE_MIN )
  values ( :Main.ID_PRODUTO, :COR, :TAMANHO, :SKU, :ESTOQUE_MIN );
#end;

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.

#for :grade_produto
do
#begin
  #iif( :ID_PRODUTO_SKU = null then
    TRUE:{
      insert into PRODUTO_SKU ( ID_PRODUTO, COR, TAMANHO, SKU, ESTOQUE_MIN )
      values ( :ID_PRODUTO, :COR, :TAMANHO, :SKU, :ESTOQUE_MIN );
    },
    ELSE:{
      update PRODUTO_SKU set COR = :COR, TAMANHO = :TAMANHO, SKU = :SKU, ESTOQUE_MIN = :ESTOQUE_MIN
      where ID_PRODUTO_SKU = :ID_PRODUTO_SKU;
    })
#end;

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).
  • Script que grava ou chama algo externo: insert, update, #CallHTTP ( ), #CallNodeJS ( ) ou #CallDLL ( ). Uma resposta guardada não executaria essas ações, então o cache é ignorado.

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.
    • Exemplo Resultado na documentação
      <b>Status Pedido</b>
      <br>
      <b>1 - </b>Aprovado
      <br>
      <b>2 - </b>Em Separacao
      <br>
      <b>3 - </b>Despachado
      <br>
      <b>4 - </b>Entregue
      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:

#SetFormatJson(:alias)
#SetFormatJson(:alias.campo)
#SetFormatJson(nome1 = :alias.campo1, nome2 = :alias.campo2, nome3 = :alias.campo3)

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.

#CallNodeJS:job(
    Module: "processarItensPedido",
    Params: { idPedido = :ID_PEDIDO, itens = #SetFormatJson(:ITENS_PED) },
    TimeOut: 10 { ... },
    Error: { ... },
    Success: { ... }
);

(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.

#SetFormatJson(:ITENS_PED.cod_produto)
-- [{"cod_produto":"7894900011517"}, {"cod_produto":"7891000100103"}, {"cod_produto":"7896004004501"}]

Vários campos escolhidos — #SetFormatJson(nome1 = :alias.campo1, nome2 = :alias.campo2, ...)

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.

#CallNodeJS:job(
    Module: "processarItensPedido",
    Params: {
        idPedido = :ID_PEDIDO,
        itens = #SetFormatJson(
            cod_pedido = :ITENS_PED.cod_pedido,
            nome_cliente = :ITENS_PED.nome_cliente,
            data_emissao = :ITENS_PED.data_emissao
        )
    },
    TimeOut: 10 { ... },
    Error: { ... },
    Success: { ... }
);
-- itens vira:
-- [
--   {"cod_pedido": "1", "nome_cliente": "João", "data_emissao": "2026-01-01"},
--   {"cod_pedido": "2", "nome_cliente": "Maria", "data_emissao": "2026-01-02"}
-- ]

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:

#SetFormatJson(
    codigo_interno = :ITENS_PED.cod_produto,
    cliente = :ITENS_PED.nome_cliente,
    data_emissao = :ITENS_PED.data_emissao
)
-- [{"codigo_interno": "7894900011517", "cliente": "João", "data_emissao": "2026-01-01"}, ...]

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:

#SetFormatJson(
    cod_produto = :ITENS_PED.cod_produto,
    quantidade = :ITENS_PED.quantidade,
    TipoProd = 'manual',
    Origem = crudapi
)
-- [{"cod_produto": "7894900011517", "quantidade": 3, "TipoProd": "manual", "Origem": "crudapi"}, ...]

Parâmetro do próprio endpoint — :param sem .campo

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):

#SetFormatJson(
    cod_produto = :ITENS_PED.cod_produto,
    idPedido = :ID_PEDIDO,
    Origem = crudapi
)

Só literais/parâmetros — nenhum :alias.campo

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):

#SetFormatJson(idPedido = :ID_PEDIDO, TipoProd = 'manual', Origem = crudapi)
-- [{"idPedido": "482", "TipoProd": "manual", "Origem": "crudapi"}]

#iif / #case embutido no valor de um campo

O valor de qualquer campo dessa forma também aceita #iif/#case embutido — 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:

#SetFormatJson(
    cod_produto = :ITENS_PED.cod_produto,
    origem = #iif(:CANAL = 'MKT') 'marketplace' #else 'crudapi' #end
)

#iif ( ) / #case ( )

Script Suportado: Endpoint e ObjectArray

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.

Valores aceitos

Valor Exemplo
Literal #SetVar(:total = 0); / #SetVar(:situacao = 'ok');
Parâmetro, alias ou outra variável #SetVar(:limite = :MAX_PAGINAS); / #SetVar(:paginas = :PRIMEIRA.total_pages);
Conta com números (+ - * /) #SetVar(:total = :total + 1); / #SetVar(:valor = :preco * :qtd);
Junção de texto (||) #SetVar(:msg = 'Página ' || :pag || ' importada');

Regras

Regra Comportamento
Onde vale No script inteiro a partir do #SetVar, inclusive dentro de #For ( ), #iif ( ) / #case ( ) e dos blocos TimeOut/Error/Success
Nome igual a um parâmetro ou alias HTTP 500
Variável usada antes do primeiro #SetVar HTTP 500
Conta com valor que não é número, ou divisão por zero HTTP 500
Devolver no Result A variável não entra sozinha na resposta; use #SetVarResult ( ), ex.: #SetVarResult(TOTAL = :total);

Exemplo — parar depois de 3 falhas

#SetVar(:erros = 0);
#SetVar(:importados = 0);

#For() :pag = 1 to 50 do
#begin
  #HttpNewUrl('https://api.exemplo.com.br/produtos');
  #HttpParam('page', :pag);
  #CallHTTP:PAGINA(
      TimeOut: 15 {
          #SetVar(:erros = :erros + 1);
          #iif(:erros >= 3 then
              TRUE: { #SetResponseCode(504); #Break; },
              ELSE: { #SetResponseCode(200); }
          );
      },
      Error: {
          #SetVar(:erros = :erros + 1);
          #iif(:erros >= 3 then
              TRUE: { #SetResponseCode(400); #Break; },
              ELSE: { #SetResponseCode(200); }
          );
      },
      Success: {
          #SetImportJsonArray(:prod, :PAGINA.items, null);
          #For() :prod do
          #begin
            insert into PRODUTO_IMPORTADO (CODIGO, DESCRICAO) values (:prod.sku, :prod.name);
            #SetVar(:importados = :importados + 1);
          #end;
      }
  );
#end;

#SetVarResult(IMPORTADOS = :importados);
#SetVarResult(FALHAS = :erros);

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 select sem 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):

  1. Na aba de Parâmetros do endpoint, declare DE e ATE como inteiros e opcionais (sem Required).
  2. 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.

#StreamUrl( <api>.api.<menu>.<método>.<endpoint>( [nome = valor, ...] ) [, thumb] )
  • 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:

#StreamUrl(api_name.api.Catalogo.Produtos.Listar_fotos( namecompany = 'acme' ), thumb)

   →  '/api_name/api/Catalogo/Produtos/Listar_fotos?namecompany=acme&thumb=true&$filename='

Exemplo — foto no JSON de uma lista

select p.cod_produto, p.descricao,
       #StreamUrl(api_name.api.Catalogo.Produtos.Listar_fotos())
         || p.cod_produto || '_' || p.cod_cor || '_01.jpg' as foto
from produtos p
{ "values": [ {
    "cod_produto": "1001",
    "descricao": "PRODUTO DE EXEMPLO AZUL",
    "foto": "/api_name/api/Catalogo/Produtos/Listar_fotos?thumb=false&$filename=1001_AZ_01.jpg"
} ] }

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.
  • A macro só monta o endereço. Quem abre a URL precisa ter acesso ao endpoint Stream: Anonymous Access, AccessToken ou usuário e senha, como em qualquer outro endpoint.
  • 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.

Uso simples — sem condição

#SetVarResult(status = 'Emitida');
#SetVarResult(chave = :RESP.chaveAcesso);

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:

#SetVarResult(itens = #SetFormatJson(produto = :itens.produto, qtd = :itens.qtd));

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.

#CallNodeJS:RESP(
    Module: "consultar_cep",
    Params: { cep = :CEP },
    TimeOut: 10 {
        #SetVarResult(status = 'timeout');
        #SetResponseCode(504);
    },
    Error: {
        #SetVarResult(status = 'erro');
        #SetVarResult(motivo = #Node_Error(message));
        #SetResponseCode(504);
    },
    Success: {
        #SetVarResult(status = 'ok');
        #SetVarResult(logradouro = :RESP.logradouro);
        #SetVarResult(bairro = :RESP.bairro);
        #SetVarResult(cidade = :RESP.cidade);
        #SetVarResult(uf = :RESP.uf);
    }
);
⚠️ 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 (produto e cor). 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 ObjectArray COR_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 #RETURN també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.

Exemplo — somando com #SetVar

#SetVar(:total = 0);

#For() :item do
#begin
  #SetVar(:total = :total + :item.valor);
  #iif(:total > 1000 then TRUE: { #Break; }, ELSE: { });
#end;

#SetVarResult(TOTAL = :total);

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):

#CreateTemporaryTable:TBL(
  PRODUTO INTEGER,
  COR INTEGER,
  TAMANHO VARCHAR(5),
  QUANTIDADE DOUBLE PRECISION
)
#createindex (PRODUTO)
#createindex (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:

#Call:res crudapi.api.pedidos.pedido_integrado.atualizar({
    idPedido = :ID_PEDIDO,
    itens = #SetFormatJson(cod_produto = :ITENS_PED.cod_produto, quantidade = :ITENS_PED.quantidade, valor_unitario = :ITENS_PED.valor_unitario)
});

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:

#Call:correio crudapi.api.pedidos.pedido_integrado.listar_embalagens(chave = :chave_empresa);

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.
#CallHTTP:SALDO(
    BodyPayload:    Json { ... },
    TimeOut: 15 { ... },
    Error:   { ... },
    Success: { ... }
);
#HttpNewUrl(endereco);
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.
#HttpHeader('Authorization', 'Bearer ' || :CLIENTES.TOKEN_API);
#HttpBasic(usuario, senha);
Troca a autorização da Config API External por Basic com este usuário e senha, só na próxima chamada. A codificação em Base64 é feita automaticamente.
#HttpBasic(:CLIENTES.USUARIO_API, :CLIENTES.SENHA_API);
#HttpBearer(token);
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.
#SetVar(:total = #HttpReceivedHeader(:PED, 'X-Total-Count'));
BodyPayload: tipo { ... }
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 ( ).

#SetVar(:restantes = #HttpReceivedHeader(:PED, 'X-RateLimit-Remaining'));
#iif(:restantes < 5 then TRUE: { #IntervalMS(1000); }, ELSE: { });

Exemplo — consulta (GET)

#HttpAddUrl('/produtos/saldodeestoque');
#HttpParam('produto', :PRODUTO);
#HttpParam('vitrine', 201);
#HttpParam('trans_id', :TRANS_ID);

#CallHTTP:SALDO(
    TimeOut: 15 {
        #SetVarResult(status = 'Consulta em andamento');
    },
    Error: {
        #SetVarResult(motivo = #Http_Error(message));
        #SetResponseCode(502);
    },
    Success: {
        #SetVarResult(saldo = :SALDO.saldo);
    }
);

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 ||:

#HttpAddUrl('/pedidos');

#HttpAddUrl('/pedidos/' || :ID_PEDIDO);

#HttpAddUrl('/clientes/' || :CLIENTE.codigo || '/pedidos/' || :PEDIDO.numero || '/itens');

#HttpAddUrl('/produtos/saldodeestoque');
#HttpParam('produto', :PRODUTO);
#HttpParam('vitrine', 201);
#HttpParam('trans_id', :TRANS_ID);

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:
    #HttpHeader('Authorization', 'Bearer ' || :CLIENTES.TOKEN_API);
    #HttpParam('cliente', :CLIENTES.CODIGO);
  • 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:
    #HttpHeader(Authorization = 'Bearer ' || :CLIENTES.TOKEN_API, 'X-Api-Key' = :CLIENTES.CHAVE_API);
    #HttpParam(cliente = :CLIENTES.CODIGO, loja = :CLIENTES.LOJA);

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:

#HttpHeader('Authorization', 'Bearer ' || :CLIENTES.TOKEN_API);
#HttpHeader('X-Api-Key', :CLIENTES.CHAVE_API);
#HttpHeader('Accept-Language', 'pt-BR');

#HttpParam('cliente', :CLIENTES.CODIGO);
#HttpParam('loja', :CLIENTES.LOJA);
#HttpParam('pagina', 1);

#HttpAddUrl('/produtos/saldodeestoque');
#CallHTTP:SALDO( ... );

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:

BodyPayload: Json {
    numero = :NUMERO_PEDIDO,
    itens  = #SetFormatJson(sku = :ITENS.cod_produto, qtd = :ITENS.qtde, preco = :ITENS.preco)
}

Número ou texto no JSON

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:

#HttpNewUrl(https://api.exemplo.com.br/v1/produtos/?ativo=True&page=1);
#HttpHeader(Content-Type, application/json);

#CallHTTP:SALDO(
    TimeOut: 15 {
        #SetVarResult(status = 'Consulta em andamento');
    },
    Error: {
        #SetVarResult(motivo = #Http_Error(message));
        #SetResponseCode(502);
    },
    Success: {
        #SetVarResult(status = 'Sucesso');
        #SetVarResult(motivo = 'comando executado');
        #SetVarResult(RESPONSE = #SetFormatJson(:SALDO));
    }
);

Exemplo — envio (POST) com lista de itens

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).

#SetImportJsonArray(:alias_novo, fonte_json, ArrayName);

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:

-- resposta: { "name": "...", "address": { "street": "...", "city": "Gwenborough" } }
#SetImportJsonArray(:ENDER, #SetFormatJson(:USR), 'address');
#SetVarResult(CIDADE = :ENDER.city);

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"):

#CallNodeJS:RESP(
    Module: "consultarPedidoExterno",
    Params: { pedido = :PEDIDO },
    TimeOut: 10 { ... },
    Error: { ... },
    Success: {
        #SetImportJsonArray(:itens, :RESP.itens, null);
    }
);

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:

{
  "centro_custos": [ { "centro_custos": 10, "descricao": "text", "ativo_sistema_legado": "S" } ],
  "plano_contas":  [ { "pconta": 10, "plano_conta": "text", "flag_interno": 0 } ],
  "tipo_pgto":     [ { "tipo_pgto": 10, "descricao": "text", "cod_legado_erp": "X1" } ]
}

O endpoint quer devolver só CENTRO_CUSTOS, PLANO_CONTAS e TIPO_PGTO — sem os campos internos do sistema legado:

#CallNodeJS:RESP(
    Module: "consultarCadastrosFinanceiro",
    Params: { empresa = :CHAVE_EMPRESA },
    TimeOut: 15 { ... },
    Error: { ... },
    Success: {
        #SetImportJsonArray(:centro_custos, :RESP.centro_custos, null);
        #SetImportJsonArray(:plano_contas, :RESP.plano_contas, null);
        #SetImportJsonArray(:tipo_pgto, :RESP.tipo_pgto, null);

        #SetVarResult(CENTRO_CUSTOS = #SetFormatJson(centro_custos = :centro_custos.centro_custos, descricao = :centro_custos.descricao));
        #SetVarResult(PLANO_CONTAS = #SetFormatJson(pconta = :plano_contas.pconta, plano_conta = :plano_contas.plano_conta));
        #SetVarResult(TIPO_PGTO = #SetFormatJson(tipo_pgto = :tipo_pgto.tipo_pgto, descricao = :tipo_pgto.descricao));
    }
);

Resposta do endpoint:

{
  "values": {
    "CENTRO_CUSTOS": [ { "centro_custos": 10, "descricao": "text" } ],
    "PLANO_CONTAS":  [ { "pconta": 10, "plano_conta": "text" } ],
    "TIPO_PGTO":     [ { "tipo_pgto": 10, "descricao": "text" } ]
  }
}

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:

[
  {
    "id": 1234567890,
    "name": "1001",
    "total_price_set": {
      "shop_money": { "amount": "78.42", "currency_code": "BRL" },
      "presentment_money": { "amount": "78.42", "currency_code": "BRL" }
    },
    "subtotal_price_set": {
      "shop_money": { "amount": "59.42", "currency_code": "BRL" },
      "presentment_money": { "amount": "59.42", "currency_code": "BRL" }
    },
    "line_items": [
      {
        "title": "Produto Exemplo",
        "sku": "SKU-001",
        "quantity": 1,
        "price": "69.90",
        "price_set": {
          "shop_money": { "amount": "69.90", "currency_code": "BRL" },
          "presentment_money": { "amount": "69.90", "currency_code": "BRL" }
        }
      }
    ],
    "shipping_lines": [
      { "title": "Sedex", "price": "19.00" }
    ]
  }
]

Exemplo — importação de pedidos da Shopify

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.
  • Qualquer campo de data de um JSON importado por #SetImportJsonArray ( ).
#SetISO8601ToDateTime(:alias.data)

Argumento

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:

#SetVarResult(criado_em = #SetFormatDateTime(:pedido.data, 'yyyy-mm-dd"T"hh:nn:ss"-03:00"'));
-- "criado_em": "2026-07-27T12:00:00-03:00"

#SetUnixToDateTime ( ) / #SetDateTimeToUnix ( )

Script Suportado: Somente Endpoint

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.
#SetUnixToDateTime(:alias.created)
#SetUnixToDateTime(:alias.created, '-03:00')

Argumentos

  • 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.
#SetDateTimeToUnix(:alias.data)
#SetDateTimeToUnix(:alias.data, 'ms')
#SetDateTimeToUnix(:alias.data, 's', '-03:00')

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 ( ).
  • 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.

Exemplo

#SetVarResult(criado = #SetDateTimeToUnix(:pedido.data));
-- "criado": 1785153600

#SetVarResult(criado = #SetDateTimeToUnix(:pedido.data, 'ms', '-03:00'));
-- "criado": 1785164400000   (12:00 em -03:00 = 15:00 UTC)

No Result

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>ToDateTime lê 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) #SetISO8601ToDateTime ( ) não precisa — é o padrão 2026-07-27T12:00:00
2026-07-27T12:00:00-03:00
Timestamp Unix (segundos, milissegundos, micro ou nanossegundos) #SetUnixToDateTime ( ) #SetDateTimeToUnix ( ) 1785153600
1785153600000
Número serial (Delphi TDateTime, Excel) #SetOADateToDateTime ( ) #SetDateTimeToOADate ( ) 46230.5
Data JSON da Microsoft (ASP.NET/WCF) #SetMsJsonToDateTime ( ) #SetDateTimeToMsJson ( ) /Date(1785153600000-0300)/
Ticks do .NET #SetTicksToDateTime ( ) #SetDateTimeToTicks ( ) 639207504000000000
FILETIME do Windows (Active Directory, LDAP) #SetFileTimeToDateTime ( ) #SetDateTimeToFileTime ( ) 134296272000000000

Ler — o macro é usado direto no lugar do valor, na hora de gravar:

insert into PEDIDO_VENDA (COD_PEDIDOV, DATA_EMISSAO)
values (:pedido.id, #SetISO8601ToDateTime(:pedido.created_at));

Escrever — o macro monta o campo que vai para o JSON de resposta:

#SetVarResult(criado = #SetDateTimeToUnix(:pedido.data));

Fuso horário

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.

Para ler ou escrever uma data em texto com um formato à sua escolha, por uma máscara, ver Macros de Formatação (#SetStrToDateTime ( ), #SetFormatDateTime ( )).

#SetOADateToDateTime ( ) / #SetDateTimeToOADate ( )

Script Suportado: Somente Endpoint

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.
#SetOADateToDateTime(:alias.data)

Argumento

Formato reconhecido

Descrição No JSON
Só data 44145
Data e hora (a parte decimal é a hora) 44145.375

Retorno

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.

Exemplo

#SetVarResult(data = #SetDateTimeToOADate(:pedido.data));
-- "data": 46230.5

No Result

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.

#SetMsJsonToDateTime ( ) / #SetDateTimeToMsJson ( )

Script Suportado: Somente Endpoint

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(...)/.
#SetMsJsonToDateTime(:alias.data)
#SetMsJsonToDateTime(:alias.data, '-03:00')

Argumentos

  • valor: :PARAMETRO, :alias.campo, outro macro de formatação ou #ParamTokenValue ( ) — nunca um literal solto.
  • 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.
#SetDateTimeToMsJson(:alias.data)
#SetDateTimeToMsJson(:alias.data, '-03:00')

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 ( ).
  • 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.

Exemplo

#SetVarResult(data = #SetDateTimeToMsJson(:pedido.data, '-03:00'));
-- "data": "/Date(1785164400000-0300)/"

No Result

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.
#SetTicksToDateTime(:alias.data)

Argumento

Formato reconhecido

Descrição No JSON
Ticks do .NET 637405956690000000

Retorno

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.

Exemplo

#SetVarResult(data = #SetDateTimeToTicks(:pedido.data));
-- "data": 639207504000000000

No Result

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.

#SetFileTimeToDateTime ( ) / #SetDateTimeToFileTime ( )

Script Suportado: Somente Endpoint

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.
#SetFileTimeToDateTime(:alias.data)
#SetFileTimeToDateTime(:alias.data, '-03:00')

Argumentos

  • valor: :PARAMETRO, :alias.campo, outro macro de formatação ou #ParamTokenValue ( ) — nunca um literal solto.
  • 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.
#SetDateTimeToFileTime(:alias.data)
#SetDateTimeToFileTime(:alias.data, '-03:00')

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 ( ).
  • 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.

Exemplo

#SetVarResult(alterado = #SetDateTimeToFileTime(:pedido.data));
-- "alterado": 134296272000000000

No Result

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:

<nfeProc versao="4.00" xmlns="http://www.portalfiscal.inf.br/nfe">
  <NFe>
    <infNFe Id="NFe3526100000000000019155001000001234..." versao="4.00">
      <ide> <nNF>1234</nNF> <serie>1</serie> ... </ide>
      <emit> <CNPJ>00000000000191</CNPJ> <xNome>EMPRESA VENDEDORA EXEMPLO LTDA</xNome> ... </emit>
      <dest> <CNPJ>11111111000111</CNPJ> <xNome>EMPRESA COMPRADORA EXEMPLO LTDA</xNome> ... </dest>
      <det nItem="1">
        <prod>
          <cProd>PRD-001</cProd>
          <xProd>PRODUTO EXEMPLO A</xProd>
          <qCom>1</qCom>
          <vProd>299.00</vProd>
          <xPed>00012345</xPed>
          <nItemPed>0001</nItemPed>
        </prod>
        <imposto>
          <ICMS>
            <ICMS00> <orig>0</orig> <CST>00</CST> <vBC>299.00</vBC> <pICMS>18.00</pICMS> <vICMS>53.82</vICMS> </ICMS00>
          </ICMS>
          <PIS>
            <PISAliq> <CST>01</CST> <vBC>299.00</vBC> <pPIS>1.65</pPIS> <vPIS>4.93</vPIS> </PISAliq>
          </PIS>
          <COFINS>
            <COFINSAliq> <CST>01</CST> <vBC>299.00</vBC> <pCOFINS>7.60</pCOFINS> <vCOFINS>22.72</vCOFINS> </COFINSAliq>
          </COFINS>
        </imposto>
      </det>
      <det nItem="2">
        <prod>
          <cProd>PRD-002</cProd>
          <xProd>PRODUTO EXEMPLO B</xProd>
          <qCom>2</qCom>
          <vProd>280.66</vProd>
          <xPed>00012345</xPed>
          <nItemPed>0002</nItemPed>
        </prod>
        <imposto>
          <ICMS>
            <ICMS00> <orig>0</orig> <CST>00</CST> <vBC>280.66</vBC> <pICMS>18.00</pICMS> <vICMS>50.52</vICMS> </ICMS00>
          </ICMS>
          <PIS>
            <PISAliq> <CST>01</CST> <vBC>280.66</vBC> <pPIS>1.65</pPIS> <vPIS>4.63</vPIS> </PISAliq>
          </PIS>
          <COFINS>
            <COFINSAliq> <CST>01</CST> <vBC>280.66</vBC> <pCOFINS>7.60</pCOFINS> <vCOFINS>21.33</vCOFINS> </COFINSAliq>
          </COFINS>
        </imposto>
      </det>
      <total>
        <ICMSTot>
          <vBC>579.66</vBC> <vICMS>104.34</vICMS> <vProd>579.66</vProd>
          <vPIS>9.56</vPIS> <vCOFINS>44.05</vCOFINS> <vNF>579.66</vNF>
        </ICMSTot>
      </total>
    </infNFe>
  </NFe>
  <protNFe versao="4.00"> <infProt> <chNFe>...</chNFe> <cStat>100</cStat> ... </infProt> </protNFe>
</nfeProc>

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:

select:doc XML_NOTA from NOTA_ENTRADA where ID = :ID;

#SetImportXml(:ide,  :doc.XML_NOTA, 'ide');
#SetImportXml(:emit, :doc.XML_NOTA, 'emit');
#SetImportXml(:det,  :doc.XML_NOTA, 'det');

#For() :det do
#begin
  #SetImportXml(:prod, :det.prod, null);
  #SetImportXml(:icms,   :det.imposto, 'ICMS00');
  #SetImportXml(:pis,    :det.imposto, 'PISAliq');
  #SetImportXml(:cofins, :det.imposto, 'COFINSAliq');

  insert into NOTA_ENTRADA_ITEM (ID_NOTA, NUMERO_NF, CNPJ_EMITENTE, ITEM, CODIGO, DESCRICAO, QTDE, VALOR,
                                 BASE_ICMS, ALIQ_ICMS, VALOR_ICMS, VALOR_PIS, VALOR_COFINS)
  values (:ID, :ide.nNF, :emit.CNPJ, :det.nItem, :prod.cProd, :prod.xProd, :prod.qCom, :prod.vProd,
          :icms.vBC, :icms.pICMS, :icms.vICMS, :pis.vPIS, :cofins.vCOFINS);
#end;

O que cada alias contém

CampoVem deValor no exemplo
:ide.nNF'ide' — uma linha1234
:emit.CNPJ'emit' — uma linha00000000000191
:det.nItem'det' — uma linha por item (atributo)1 e 2
:det.prod'det' — bloco dentro do blocoo XML <prod>...</prod> do item
:prod.cProd:det.prod com nullPRD-001 / PRD-002
:icms.vICMS:det.imposto com 'ICMS00'53.82 / 50.52
:pis.vPIS:det.imposto com 'PISAliq'4.93 / 4.63
:cofins.vCOFINS:det.imposto com 'COFINSAliq'22.72 / 21.33

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:

#HttpNewUrl(https://api.exemplo.com.br/nfse/ws);
#HttpHeader(SOAPAction, "http://www.exemplo.com.br/ConsultarNfse");

#CallHTTP:NFSE(
    BodyPayload: Text 'text/xml; charset=utf-8' {
      <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
        <soap:Body><ConsultarNfse><Prestador><Cnpj>:CNPJ</Cnpj></Prestador>
        <NumeroNfse>:NUMERO</NumeroNfse></ConsultarNfse></soap:Body>
      </soap:Envelope> },
    TimeOut: 30 { #SetResponseCode(504); },
    Error:   { #SetVarResult(motivo = #Http_Error(message)); #SetResponseCode(502); },
    Success: {
        #SetImportXml(:notas, :NFSE, 'InfNfse');
        #For() :notas do
        #begin
          #SetVarResult(numero = :notas.Numero, verificacao = :notas.CodigoVerificacao);
        #end;
    }
);

Erros

  • :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).
  • #SetFormatDateTime ( ) — escreve uma data na máscara pedida, ex.: dd/mm/yyyy.

#SetToFloat ( )

Script Suportado: Endpoint e ObjectArray

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));

Para controlar casas decimais depois de converter, componha com #SetToRound ( ) ou #SetToTrunc ( ):

#SetToRound( #SetToFloat(:PESO), 2 )

#SetToRound ( )

Script Suportado: Endpoint e ObjectArray

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.

#SetToTrunc(:VALOR, 2)
-- 10,569 -> 10.56
-- -10,569 -> -10.56

Exemplo gravando um valor truncado em duas casas, a partir de um parâmetro do endpoint:

update PRODUTO set PESO_LIQUIDO = #SetToTrunc(:PESO_BRUTO, 2) where PRODUTO = :PRODUTO;

E a partir de um campo de alias, dentro de um #For:

insert into ITEM_PEDIDO (PEDIDO_VENDA, PRODUTO, QUANTIDADE)
values (:PEDIDO_VENDA, :item.PRODUTO, #SetToTrunc(:item.QUANTIDADE, 3));

#SetToInteger ( )

Script Suportado: Endpoint e ObjectArray

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).

#SetToInteger(:QUANTIDADE)
-- 4,9 -> 4
-- -4,9 -> -4

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.

#SetStrToDateTime(:alias.data, 'dd/mm/yyyy')
#SetStrToDateTime(:alias.data, 'dd/mm/yyyy hh:mm:ss')

Para que serve

  • 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 ( )).

#SetFormatDateTime(:alias.data, 'dd/mm/yyyy')
#SetFormatDateTime(:DATA_ENTREGA, 'dd/mm/yyyy hh:mm:ss')

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

Para a data 2026-07-27T14:30:00:

#SetFormatDateTime(:DATA, 'dd/mm/yyyy')              -- 27/07/2026
#SetFormatDateTime(:DATA, 'dd-mm-yyyy')              -- 27-07-2026
#SetFormatDateTime(:DATA, 'dd/mm/yyyy hh:mm:ss')     -- 27/07/2026 14:30:00
#SetFormatDateTime(:DATA, 'dd.mm.yyyy hh:mm:ss')     -- 27.07.2026 14:30:00
#SetFormatDateTime(:DATA, 'yyyy-mm-dd"T"hh:mm:ss')   -- 2026-07-27T14:30:00
#SetFormatDateTime(:DATA, 'hh:mm:ss.zzz')            -- 14:30:00.000

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:

#CallNodeJS:RESP(
    Module: "consultarPedidoExterno",
    Params: { data = #SetFormatDateTime(:DATA, 'dd/mm/yyyy') },
    TimeOut: 10 { ... },
    Error: { ... },
    Success: {
        #SetVarResult(data_br = #SetFormatDateTime(:RESP.data, 'dd/mm/yyyy'));
    }
);

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):

#SetFormatDateTime( #SetStrToDateTime(:alias.data, 'dd/mm/yyyy'), 'yyyy-mm-dd' )   -- 27/07/2026 -> 2026-07-27

O que o macro não faz: ele formata valores já resolvidos (parâmetro, alias, token) — não aceita o nome de uma coluna dentro do SQL.