Web Framework

Perguntas frequentes sobre o Web Framework.

01. Como forçar o uso do HTTPS no Web Framework?

Para forçar o uso do HTTPS, deve-se criar um arquivo x-class ou x-view no diretório “/Configuração/Web Framework/Segurança” da Virtual File System.

Nesta classe, pode-se indicar manualmente os servidores que têm certificados SSL instalados e são capazes de atender o protocolo HTTPS, conforme exemplo abaixo:

this.httpsCapable = true;
this.serverAddress.push('<IP_DO_SERVIDOR>');

Caso o protocolo HTTPS seja implementado por um outro serviço, como um balanceador de carga ou um proxy reverso, pode-se utilizar o evento o onGetUseHTTPS para detectar que o tráfego foi originado a partir desse outro serviço e realizar o redirecionamento automático. Abaixo segue um exemplo que detecta o uso do balanceador de carga do Google Cloud Platform:

this.onGetUseHTTPS.set(function() {
  return request.getHeader('via') === '1.1 google' &&
    request.getHeader('x-forwarded-proto') === 'http';
});

02. Como adicionar colunas ao processo de permissões?

Durante o processo de desenvolvimento, quando trabalhando em novas funcionalidades para o sistema às vezes surge a necessidade de criar uma permissão específica para uma ação definida nesta funcionalidade. O processo de definição de permissões pertence à licença do time Framework, então para resolver o nosso problema isoladamente, existe uma maneira isolada e simples de criar essa coluna sem alterar esse processo.

O primeiro passo a se dar é criar a nova coluna para a nossa nova permissão na tabela de permissões, por isso vamos à classe Permissões em “Raiz > Dados > Sistema > Permissões”. Com essa classe em foco vamos criar um novo registro contendo os dados do nosso campo.

var fld = this.field("EDITANOMECLIENTE", "boolean");
fld.label = "Permite edição de nomes de clientes em um processo imaginário";
fld.help = "Informe se o usuário ou grupo tem permissão para alterar os nomes dos clientes imaginários no processo imaginário.";
fld.order = 999;
fld.visible = true;
fld.scope.addClass(-123456678);

Você deve ter notado que chamamos o método addClass do propriedade scope que, como o nome já diz, adiciona as classes passadas por parâmetro dentro do escopo deste campo de permissão. A consequência disso é que este campo estará visível no processo de permissões para estas classes e suas descendentes. Essa é a única maneira de alterar a visibilidade dos campos de permissão. Apesar de não recomendarmos, é possível um campo de permissão ter várias classes raízes, basta chamar o método novamente passando uma nova chave. Nos casos em que é necessário a criação de uma nova raiz para o campo de permissão é preferível que se crie um novo campo.

IMPORTANTE: Quando dizemos que a visibilidade dos campos de permissão não deve ser alterada em nenhum lugar, nem mesmo no evento onDefinePermissionsGrid, estamos falando sério. Caso isso aconteça um erro será disparado.

Feito isso, certifique-se de que a coluna está criada no banco de dados e no cache local, e prossiga com o nosso manual.

Agora que temos o campo criado e visível, pode ser necessário ainda alguma customização. Como temos acesso aos campos? Simples! É só definir uma implementação do evento onDefinePermissionsGrid na classe que se deseja essas alterações. Segue um exemplo:

this.onDefinePermissionsGrid.set(function (grid) {
  inherited(grid);
  var fld = grid.field('editanomecliente');
  fld.label('lambda');
  fld.help('Permissão para escrever "lambda"!');
});

IMPORTANTE: Depois de ler sobre o evento onDefinePermissionsGrid você pode querer alterar a visibilidade dos campos de permissão utilizando-o. Isto não é possível. A visibilidade dos campos só pode ser alterada utilizando o método o método addClass do propriedade scope do próprio campo.


03. Como alterar o texto da barra de título do navegador?

Para configurar o título exibido no navegador durante o uso do sistema deve ser informada uma expressão no campo “Título do navegador” do processo “Administração do sistema > Aparência e personalização > Ambiente”. Podem ser utilizadas as variáveis abaixo para injetar valores dinâmicos:

  • $DataBase: nome da base de dados.
  • $Port: porta de acesso em que o sistema está sendo executado.
  • $EngineVersion: versão atual do Engine.
  • $ProductVersion: versão atual do sistema.
  • $LoggedUser: usuário logado no sistema.
  • $SystemName: nome do sistema.

Exemplo:

  • Título: Sistema - $ProductVersion $DataBase/$LoggedUser - Engine: $EngineVersion - Porta: $Port.
  • Resultado: Sistema - 2025.3 BASE/usuario - Engine: 71.0.2 - Porta: 80.

04. Como criar um relatório em árvore?

Árvore é uma estrutura capaz de representar uma hierarquia em modo gráfico, sendo a sua base, a raiz, o primeiro elemento e também a origem de todos os outros elementos. As pontas da árvore são as folhas.

E como eu indico ao sistema que o meu relatório se trata de um relatório em árvore? O Simple Layout vai entender isso através do método sobrecarregado newRecord. A assinatura do método é essa:

newRecord(checkGroup, groupTotalLabel, showTopLine, showBottomLine, treeNodeId, parentTreeNodeId)

Note que os dois últimos parâmetros dizem respeito à estrutura de árvore, sendo o treeNodeId o ID do nó que se quer acrescentar ao Simple Layout e o parentTreeNodeId o pai dele.

O Simple Layout ainda tem atributos especiais para a nossa estrutura em árvore:

  • treeExpansionLevel: Esse atributo é um inteiro que representa até que nível da árvore ela vai estar expandida quando o relatório for aberto.
  • showTreeRoot: Atributo booleano que indica se a raiz da árvore deve ser exibida.

05. Como estender o processo Explorador de classes?

O processo “Explorador de classes” é utilizado para configurar as classes presentes no sistema e foi desenvolvido de forma que seja possível configurar as classes existentes no sistema. A execução do processo abrirá uma tela onde mostrará toda a hierarquia de classes do sistema.

É possível estendê-lo para que a hierarquia de classes exibidas, seja mostrado a partir de uma determinada classe base.

Para configuração é necessário criar um processo e alterar a propriedade baseClass para ter a chave da classe que se deseja utilizar como raiz (base). O exemplo do processo segue abaixo:

this.baseClass = -1898187809; /* Usuarios */
__includeOnce(-1898146496 /* Explorador de classes.ip */);

06. Como estender o processo Explorador de registros?

O processo “Explorador de registros” é utilizado para criação, exclusão e alteração dos registros de quaisquer tabelas de cadastro (filhas da classe Cadastrais) do sistema e também pode ser estendido e customizado.

Os atributos e eventos que podem ser customizados são:

  • viewMode: Define o modo de visualização da grade na primeira escrita. (Grid.FORMVIEW ou Grid.TABLEVIEW)
  • baseKey: Chave do registro em que a grade deve estar posicionada. (Number)
  • help: ajuda do processo. (String)
  • onDefineExplorerGrid: evento que possibilita a customização da grade de dados.

Um exemplo de um processo simples usando processo “Explorador de registros”:

this.baseClass = -1898187808; // Tipos de Arquivos
__include(-1898145512); // Explorador de registros.ip

Podemos também usar propriedades para alterar a grade, no exemplo abaixo estamos mudando o modo de exibição:

this.baseClass = -1898187808; // Tipos de Arquivos
this.viewMode = Grid.FORMVIEW;
__include(-1898145512); // Explorador de registros.ip

Pode-se fazer customizações mais específicas utilizando o evento onDefineExplorerGrid. No exemplo abaixo iremos criar um novo filtro (extensions) para a grande principal:

this.baseClass = -1898187808; // Tipos de Arquivos
__include(-1898145512); // Explorador de registros.ip

this.onDefineExplorerGrid.set(function(grid) {
  var filter = grid.process.grid("filter");
  filter.onDefineFields.set(function(grid) {
    inherited(grid);
    var fld = grid.field("extension", "string", 4);
    fld.defaultValue = null;
    fld.saveInputAsDefault = false;
    fld.help = "Extensão do arquivo.";
    fld.onAfterChange.set(function(field) {
      field.parent.process.atualizaFiltroDataSet();
    });
    var fld = grid.field("CLASS");
  });

  grid.process.atualizaFiltroDataSet();
})

this.atualizaFiltroDataSet = function() {
  this.dataSet.filter = this.pegaFiltroDaClasse();
}

this.pegaFiltroDaClasse = function() {
  var filter = this.filter;

  var extension = filter.field("extension").value;

  if (extension) {
    return "js: ds.ifileextensions == \"" + extension + "\"";
  } else {
    return '';
  }
}

07. Como executar códigos na inicialização do Web Framework?

Durante o desenvolvimento de alguns produtos, surgiu a necessidade de se executar eventos de interação com o usuário logo após o login no sistema. Para isso desenvolvemos usando um sistema de inicialização para o Framework.

Temos um novo sistema de inicialização que executa os scripts presentes na classe localizada em “/Configuração/Inicialização do Web Framework”. Nessa classe está presente a variável global de nome “environment” que é do tipo Environment.

Para sua utilização, deve ser criado um registro novo com uma chave custom na classe -1892603642 (/Configuração/Inicialização do Web Framework). Com a chave custom criada podemos inserir o nosso código diretamente.

var answer = env.confirm('Deseja usar o sistema?', false);
if (!answer) {
  env.exit();
} else {
  env.alert('Bem vindo ao sistema!');
}

08. Como alterar os valores exibidos na grade lookup?

Os valores apresentados na grade Lookup são definidos por scripts do tipo x-find (extensão .if). Para customizar os valores modificados, é necessário criar um script x-find na classe de dados a ser customizada e modificar o valores informados no terceiro parâmetro do método addResult, conforme exemplo abaixo:

while (!this.ds.eof) {
  const displayValue = this.ds.str('iCode') + ' - ' + this.ds.str('iFirstName') + ' ' +
    this.ds.str('iLastName') + ', ' + this.ds.str('iAge');
  this.addResult(this.ds.num('iKey'), this.ds.num('iClass'), showValue);
  this.ds.next();
}

Scripts x-find não são triviais de serem mantidos, portanto, avalie modificar os scripts existentes em vez de criar novos scripts com pequenas variações deles.


09. Como criar grades detalhe?

Exemplo 1: Grade simples

this.interaction("main", function () {

  this.ds = connection.cloneLocalCacheByClass(-1898187808/* Tipos de Arquivos */);
  this.ds.indexFieldNames = 'iKey';

  this.detail = dbCache.getTable('iVfs');
  this.detail.indexFieldNames = 'imimeType';

  var grid = this.grid("master", this.ds, -1898187808/* Tipos de Arquivos */);

  grid.onDefineFields.set(function(grid){
      var fld = grid.field("detail", "grid");

      fld.readOnly = true;
      fld.masterFieldNames = 'iKey';
      fld.detailFieldNames = "imimeType";
      fld.detailIndexFieldNames = "imimeType";
      fld.detailFilter = null;
      fld.masterDetailMaxRecordCount = 10;
      fld.masterDeleteAction = "throw";

      fld.onDefineGrid.set(function(field) {
          var grid = field.grid


          grid.onCreateDataSet.set(function (grid) {
              return grid.process.detail
          })
      });
  });
  grid.onBeforePost.set(function(grid) {
      if (grid.field("detail").ds.isEmpty) {
          grid.process.alert("O DataSet da grade detalhe está vazio");
      }
  });

  grid.write();

});

Exemplo 2: Utilizando eventos específicos da grade Detalhe.

Neste exemplo, ao invés de utilizarmos as propriedades, iremos sobrescrever os eventos da grade, de forma que ela produzirá o mesmo efeito das propriedades que foram definidas no exemplo 1.

this.interaction("main", function () {

  this.ds = connection.cloneLocalCacheByClass(-1898187808/* Tipos de Arquivos */);
  this.ds.indexFieldNames = 'iKey';

  this.detail = dbCache.getTable('iVfs');
  this.detail.indexFieldNames = 'imimeType';

  var grid = this.grid("master", this.ds, -1898187808/* Tipos de Arquivos */);
  grid.onDefineFields.set(function(grid){
      var fld = grid.field("detail", "grid");
      fld.order = 999;
      fld.readOnly = true;
      fld.masterDetailMaxRecordCount = 10;
      fld.onDefineGrid.set(function(field) {
          var grid = field.grid

          grid.onCreateDataSet.set(function (grid) {
              return grid.process.detail
          });

          grid.onMasterScroll.set(function (grid) {
              var master = grid.master;
              grid.ds.setRange(master.iKey,master.iKey);
          });

          grid.onMasterDelete.set(function (grid) {
              if( !grid.ds.isEmpty ) {
                  throw new Error('É necessário apagar os registros da grade detalhe antes de efetuarmos a deleção.');
              }
          });

          grid.onMasterInsert.set(function (grid) {
              grid.process.alert('Essa extensão não tem nenhum arquivo associado ainda!');
          });

          grid.onMasterPost.set(function(grid) {
              grid.process.alert('Detalhe: A master acabou de ser postada!');
          });

      });
  });

  grid.write();
});

10. Como estender a interação showVariables de um layout?

Todo relatório no sistema é, na verdade, um processo com interactions e activities pré-definidas, de acordo com a seguinte estrutura básica:

  • interaction showVariables
  • interaction run
  • interaction writeLayout

Existem ainda outras interações auxiliares responsáveis pelo envio de e-mail, exportação do layout para texto, dentre outras.

Iremos compreender como estender a interação showVariables, que é primeira interação chamada ao ser aberto um relatório, e que é responsável pela montagem da grade de variáveis visando a execução do layout.

Um caso prático:

this.help = 'Esse layout mostra como é possível estender a interação "showVariables"
           para, por exemplo, alterar os botões ou escrever um texto antes da
           grade principal.';

vars.field('variavelA', 'int64');
vars.field('variavelB', 'int64');
vars.field('variavelC', 'int64');

this.interaction( 'showVariables', function () {
  var label = this.label(this.help + '<br><br>');
  label.write();

  inherited();

  this.visibleButtons = ['run', 'NovaAção', 'printTxt', 'sendReport', 'export'];
});

this.activity('run', function () {});

this.interaction('writeLayout', function () {
  this.write('resultado');
});

this.activity('NovaAção', function () {
  throw new Error('Ação nova executada com sucesso!');
});

O trecho de código destacado em vermelho, mostra a definição (opcional) da interaction showVariables.

  1. Observe que primeiro estamos escrevendo um label com um texto obtido da propriedade help do layout.

    var label = this.label(this.help + '<br><br>');

    label.write();

  2. Em seguida, a instrução inherited(); se encarrega de executar o que por padrão a interação em questão faz: que é escrever a grade de variáveis e definir as ações padrão. Se essa instrução não for executada, a grade de variáveis não será escrita.

    inherited();

  3. Por fim, estamos redefinindo as ações que essa interação irá apresentar, acrescentando às ações já existentes, uma nova ação chamada ‘NovaAção’:

    this.visibleButtons = ['run', 'NovaAção', 'printTxt', 'sendReport', 'export'];

É importante atentar para os nomes das ações pré-definidas pelo SimpleLayout. São elas:

  • run
  • printTxt
  • sendReport
  • export

11. Como exportar os dados de um relatório que não usa SimpleLayout?

iremos mostrar o uso do evento onGetDataExporter, que serve para exportarmos dados em relatórios que não usam o SimpleLayout.

Exemplo:

includeOnce -1898143872; /* .../library/export/DataSetExporter.ijs */

var fld = vars.field('quantidadeDeRegistros', 'int32');
fld.help = 'Quantidade de registros que serão exportados.';

this.activity('run', function () {
  this.ds = new DataSet();
  this.ds.createField('Nome', 'string', 200);
  this.ds.createField('Valor', 'int32');
  this.ds.create();

  var qtdRegistros = this.quantidadeDeRegistros;
  for (var i = 0; i < qtdRegistros; ++i) {
    this.ds.append(['Registro' + i, i]);
    this.ds.post();
  }
});

this.interaction( 'writeLayout', function () {
  var result = '';
  result += '<br><table>';
  result += '<tr>';
  for (var i = 0; i < this.ds.fieldCount; ++i) {
    result += '<td>';
    result += this.ds.getFieldName(i);
    result += '</td>';
  };
  result += '</tr>';

  for (this.ds.first(); !this.ds.eof; this.ds.next()) {
    result += '<tr>';
    for (var i = 0; i < this.ds.fieldCount; ++i) {
      result += '<td>';
      result += this.ds.getField(i);
      result += '</td>';
    };
    result += '</tr>';
  }
  result += '</table>';

  this.write(result);
});

this.onGetDataToExport.set(function(process) {
  var dataExporter = new DataSetExporter(process.ds);
  dataExporter.title = process.title;

  return dataExporter;
});
  • Incluímos o objeto de exportação de dados do DataSet.
  • Declaramos as variáveis do relatório.
  • Criamos um dataSet com uma massa de dados de teste.
  • Criamos um relatório HTML sem o uso do SimpleLayout.
  • Alteramos o evento onGetDataToExport para retornar um DataSetExporter do dataSet utilizado para gerar o relatório.

12. Como funcionam os agrupamentos de campos da grade?

No modo formulário, a grade do sistema agrupa automaticamente os campos com mesmo “group” (Field.group). Vejamos o exemplo de definição de campos abaixo, e logo em seguida o resultado da ordenação dos campos.

grid.onDefineFields.set(function(grid) {
  var ordem = 0;

  var fld = grid.field("campoA", "int64");
  fld.group = "Grupo 1";

  var fld = grid.field("campoB", "int64");
  fld.group = "Grupo 2";

  var fld = grid.field("campoC", "int64");
  fld.group = "Grupo 1";
});

Os campos serão exibidos na seguinte ordem:

  • Grupo 1

    CampoA

    CampoC

  • Grupo 2

    CampoB

Observe que o campoB, apesar de ter sido definido entre os campos CampoA e CampoC, ficou isolado pois pertence a um grupo diferente.

Existem duas maneiras de definir a propriedade group de um field:

  • Ela pode ser definida com uma String. Ex.: “Primeiro Agrupamento”
  • Ou pode receber uma instância do objeto FieldGroup, como no exemplo abaixo:
var fieldGroup = new FieldGroup("Grupo Contraído");
// Todos os campos desse agrupamento estarão contraídos no momento da escrita da grade,
// podendo ser visualizados quando o usuário clicar sobre o agrupamento para expandí-lo.
fieldGroup.collapsed = true;

var fld = grid.field("campoF", "string", 30);
fld.group = fieldGroup;

var fld = grid.field("campoG", "string", 30);
fld.group = fieldGroup;

Os campos CampoF e CampoG serão visualizados dentro do “Grupo Contraído” somente após esse ser expandido manualmente


13. Como utilizar a propriedade aggregate do Field?

Essa propriedade serve para agregar campos que tenham um campo correspondente no DataSet.

Ela permite o uso de funções de:

  • Soma
  • Média
  • Máximo
  • Mínimo
  • Quantidade

O exemplo abaixo irá mostrar como realizamos cada totalização nos campos, é simples:

this.interaction("main", function () {

  this.ds = new DataSet();
  this.ds.createField('soma','number');
  this.ds.createField('media','number');
  this.ds.createField('maximo','number');
  this.ds.createField('minimo','number');
  this.ds.createField('quantidade','number');
  this.ds.create();

  this.ds.append([1,1,1,1,1]);
  this.ds.append([2,2,2,2,2]);
  this.ds.append([3,3,3,3,3]);
  this.ds.append([0,0,0,0,0]);
  this.ds.append([0,0,0,0,0]);
  this.ds.append([0,0,0,0,0]);
  //              6,1,3,0,6
  this.ds.post();

  var gr = this.grid("Agregacao", this.ds);
  gr.onDefineFields.set(function (grid) {
    var fld = grid.field("soma", "number");
    fld.label = "Soma";
    fld.aggregate = new SumDataSetAggregate();

    var fld = grid.field("media", "number");
    fld.aggregate = new AvgDataSetAggregate();
    fld.label = "Média";

    var fld = grid.field("maximo", "number");
    fld.label = "Máximo";
    fld.aggregate = new MaxDataSetAggregate();

    var fld = grid.field("minimo", "number");
    fld.label = "Mínimo";
    fld.aggregate = new MinDataSetAggregate();

    var fld = grid.field("quantidade", "number");
    fld.label = "Quantidade";
    fld.aggregate = new CountDataSetAggregate();
  });

  gr.write();
});

14. Como utilizar a propriedade canNavigate?

Ao entrar no Web Framework, o sistema precisa descobrir quais diretórios devem ser exibidos no menu Ir Para. O comportamento atual é que sejam exibidos os diretórios que possuem a propriedade de x-class canNavigate com valor true.

Para descobrir quais diretórios possuem esta propriedade habilitada, o sistema inicia uma exploração da árvore de classes a partir da classe Raiz. Se a classe não configurar esta propriedade, suas filhas são avaliadas. Este processo é executado recursivamente até que se encontre uma classe que defina a propriedade canNavigate.

Visto que habilitados a propriedade canNavigate apenas nas classes que são módulos do sistema, a princípio esta implementação teria um problema de desempenho, pois teríamos que executar praticamente todos os x-class do sistema. Para evitar este comportamento, temos a possibilidade indicar que um ramo não será navegável no menu Ir Para definindo a propriedade canNavigate com false. Desta forma, suas filhas não precisam ser exploradas.

Se observamos a nossa árvore de classes, temos 3 grandes ramos:

  • Dados
  • Configuração
  • products

Os dois primeiros não podem conter processos ou relatórios, portanto são configurados com canNavigate false. Esta configuração garante que todos os x-class dentro destes ramos não serão avaliados desnecessariamente na inicialização do Web Framework. O diretório products não possui esta configuração, pois é nele que estão os processos e relatórios dos módulos.


15. Por que os detalhes técnicos da ajuda e a pilha do erro não são exibidos pelo sistema?

Os detalhes técnicos nos diálogos de erro e de ajuda podem conter informações privilegiadas que podem ser utilizadas em um ataque, fragilizando a segurança do sistema. Por padrão, essas informações são exibidas apenas para os usuários que participam dos grupos “Administrators” e “Developers”.

Para permitir que outros grupos de usuários visualizem essas informações, basta conceder a esses grupos a permissão ao escopo “security.viewTechnicalInfo”. Essa atribuição deve ser realizada no processo “Administração do sistema > Segurança > Grupos, papéis e usuários > Grupos e papéis”. No entanto, considerando aspectos de segurança, deve-se restringir a visualização dessas informações à menor quantidade de usuários possíveis.


16. É possível declarar um processo em um módulo da Union File System?

Diretamente, não. Os processos precisam ser definidos em arquivos da Virtual File System, pois a chave do arquivo é utilizada como identificador do processo no controle de permissões do sistema e o menu de navegação é construído a partir da estrutura de diretórios da Virtual File System. No entanto, o script da Virtual File System pode delegar a definição do processo para um módulo JavaScript por meio da função loadModule conforme exemplo abaixo:

// Vfs File "/products/custom/modules/Example/Process.ip"

this.loadModule('custom/modules/Example/Process.js');
// Ufs File "/package/custom/modules/Example/Process.js"

/** @param {import('@nginstack/web-framework/lib/process/Process.js')} process */
module.exports = function (process) {

  process.help = 'Example process.';

  process.interaction('main', function () {
    const label = process.label('Hello World from module!');
    label.write();
  });
};

17. É possível realizar login automático no sistema a partir de outra página?".

Sim. Para acessar diretamente o sistema, a partir de outro site, sem precisar passar pela página de login, é necessário:

  • Obter um token de autorização com o escopo api.webFramework habilitado. A criação do token deve ser realizada no processo “Administração do sistema > Segurança > Tokens de autorização” por um usuário com permissão para tal. Para mais detalhes consulte a documentação do processo.
  • Com o token criado, adicione na página de origem do acesso um link ou botão que deverá disparar o acesso ao sistema. O endereço acessado deve ser o endereço do sistema onde o token de acesso foi criado, informando o caminho ‘/web-framework/environment’ e o parâmetro accessToken:
<a href="https://meu-sistema.com/web-framework/environment?accessToken=XXXXXX">Acessar sistema</a>

18. Como escrever grandes volumes de texto em uma coluna de um relatório?

Por padrão as colunas textuais em um relatório são limitadas a 100.000 caracteres. Caso seja necessário escrever conteúdos maiores, é possível configurar a coluna do relatório como sendo do tipo “memo”.

Vale ressaltar que colunas do tipo “memo” têm performance inferior aos campos do tipo texto. Por esse motivo, seu uso deve ser evitado quando possível.

Para configurar uma coluna como sendo do tipo “memo”, deve-se informar o tipo na chamada ao método column do SimpleLayout, conforme exemplo abaixo:

const sl = this.getSimpleLayout();
sl.column('Campo normal');
sl.column('Campo muito grande', {type: 'memo'});