Quem já trabalhou com Consumer Goods Cloud Modeler sabe que a arquitetura de 6 camadas resolve bem a parte declarativa. DataSource mapeia dados, Business Object define estrutura, Process orquestra fluxos, UI apresenta tudo ao usuário. Mas quando a regra de negócio foge do básico, é nos arquivos .bl.js que a solução mora. E é justamente nesse ponto que muitos projetos tropeçam.
Neste artigo, vou compartilhar o que aprendi implementando lógica de negócio em .bl.js em projetos reais de CGC Mobile: como funcionam os lifecycle hooks, como o ProcessContext se comporta em cada fase, quais APIs do framework estão disponíveis e, principalmente, quais armadilhas evitar.
O que são os arquivos .bl.js
No Modeler do Consumer Goods Cloud, toda lógica de negócio customizada vive em arquivos JavaScript com a extensão .bl.js. Eles ficam dentro do diretório Mv2/ de cada Business Object ou ListObject, organizados por lifecycle hook.
A estrutura de diretórios segue um padrão previsível:
src/Visit/BO/BoVisit/
BoVisit.businessobject.xml
Mv2/
LoadAsync/
BoVisit.AfterLoadAsync.bl.js
SaveAsync/
BoVisit.BeforeSaveAsync.bl.js
CreateAsync/
BoVisit.AfterCreateAsync.bl.js
Initialize/
BoVisit.Initialize.bl.js
DoValidateAsync/
BoVisit.DoValidateAsync.bl.js
BoVisit.CustomMethod.bl.js
Cada arquivo .bl.js implementa uma função que o framework invoca automaticamente quando o lifecycle correspondente dispara. A nomenclatura importa: o Modeler espera encontrar um arquivo com o nome exato do método declarado no XML do Business Object.
Os lifecycle hooks e quando cada um dispara
O framework do CGC Mobile oferece cinco lifecycle hooks principais para Business Objects, mais a possibilidade de criar métodos customizados. Cada hook tem um momento específico de execução e uma responsabilidade clara.
Initialize
Dispara quando o Business Object é instanciado pela primeira vez. É o lugar certo para definir valores padrão, configurar propriedades derivadas e preparar o estado inicial do objeto. Se você precisa que um campo tenha um valor antes de qualquer operação de load ou create, o Initialize é o ponto correto.
'use strict';
function Initialize(context) {
var me = this;
///////////////////////////////////////////////////////////////////////////////////////////////
// Add your customizing javaScript code below. //
///////////////////////////////////////////////////////////////////////////////////////////////
me.setorderStatus('Draft');
me.setisEditable(true);
///////////////////////////////////////////////////////////////////////////////////////////////
// Add your customizing javaScript code above. //
///////////////////////////////////////////////////////////////////////////////////////////////
var promise = when.resolve(context);
return promise;
}
Um erro comum aqui é tentar acessar dados do banco dentro do Initialize. O objeto acabou de ser instanciado e os dados ainda não foram carregados. Initialize serve para estado inicial, não para consulta de dados.
LoadAsync (BeforeLoadAsync / AfterLoadAsync)
O LoadAsync é o hook mais utilizado. Ele dispara quando o framework carrega um Business Object do banco de dados local (SQLite). O BeforeLoadAsync executa antes do carregamento dos dados, e o AfterLoadAsync executa depois.
O AfterLoadAsync é o ponto ideal para transformar dados que vieram do banco. Formatação de valores, cálculos derivados, carregamento de objetos aninhados baseados em condições. Na prática, é onde você vai passar mais tempo.
'use strict';
function afterLoadAsync(context) {
var me = this;
var promise = when.resolve(context);
promise = promise.then(function(context) {
// Calcula o total de itens do pedido
var items = me.getorderItems();
if (Utils.isDefined(items)) {
var total = 0;
for (var i = 0; i < items.length; i++) {
total += items[i].getquantity() * items[i].getunitPrice();
}
me.setorderTotal(total);
}
return context;
});
return promise;
}
Note o padrão com when.resolve(context) seguido de promise.then(). O framework trabalha com Promises (via biblioteca when.js), e toda operação assíncrona precisa retornar uma promise. Se você simplesmente atribuir propriedades síncronas, pode dispensar o then, mas qualquer operação que envolva consulta ao banco ou carregamento de outros objetos precisa estar dentro de uma chain de promises.
CreateAsync (BeforeCreateAsync / AfterCreateAsync)
O CreateAsync dispara quando um novo registro é criado. O BeforeCreateAsync permite preparar dados antes da persistência, e o AfterCreateAsync é útil para ações que dependem do ID recém-gerado.
Um caso típico de uso do AfterCreateAsync: gerar registros filhos automaticamente. Quando um representante cria uma nova visita, o sistema pode pré-popular as tarefas da visita com base no tipo de conta.
SaveAsync (BeforeSaveAsync / AfterSaveAsync)
O SaveAsync dispara ao persistir alterações. O BeforeSaveAsync é o momento certo para validações de último minuto, ajustes de dados e cálculos finais. O AfterSaveAsync serve para ações pós-persistência, como invalidar caches ou atualizar objetos relacionados.
'use strict';
function beforeSaveAsync(context) {
var me = this;
var promise = when.resolve(context);
promise = promise.then(function(context) {
// Garante que a data de modificação está atualizada
var now = new Date();
me.setlastModifiedDate(now.toISOString());
// Valida regra de negócio
var status = me.getorderStatus();
if (status === 'Submitted' && me.getorderTotal() === 0) {
return when.reject('Não é possível enviar um pedido com valor zero.');
}
return context;
});
return promise;
}
Perceba o uso de when.reject() para interromper o save com uma mensagem de erro. Quando você rejeita a promise dentro do BeforeSaveAsync, o framework cancela a operação e exibe a mensagem ao usuário. É a forma correta de implementar validações de negócio que dependem de lógica complexa.
DoValidateAsync
O DoValidateAsync é dedicado a validações. Diferente do BeforeSaveAsync, ele não altera dados, apenas verifica se o estado atual do objeto é válido. Se a validação falhar, o framework impede o save.
A escolha entre DoValidateAsync e BeforeSaveAsync depende do cenário. Use DoValidateAsync quando a validação é pura e não depende de transformar dados. Use BeforeSaveAsync quando precisa tanto validar quanto ajustar valores antes de salvar.
ProcessContext: o objeto compartilhado
O ProcessContext é o mecanismo de comunicação entre o Process (camada 4) e o Business Object (camada 3). Quando um processo define uma variável no ProcessContext, o .bl.js pode acessar e manipular essa variada via context.
No arquivo .processflow.xml, o ProcessContext é declarado assim:
<ProcessContext>
<Variable name="currentVisit" type="BoVisit"/>
<Variable name="taskList" type="LoVisitTask"/>
</ProcessContext>
Dentro do .bl.js, o context carrega essas variáveis. Se o processo carregou o Business Object via LOAD action, o context traz o objeto já populado. Se o processo definiu parâmetros adicionais, eles também estão disponíveis.
Uma armadilha frequente: assumir que o ProcessContext tem variáveis que não foram declaradas no processo. Se você tentar acessar context.ProcessContext.someVariable que não existe, vai receber undefined sem erro aparente. Sempre verifique com Utils.isDefined() antes de usar.
APIs do framework disponíveis no .bl.js
O framework do CGC Mobile disponibiliza several APIs globais dentro dos arquivos .bl.js. Conhecer cada uma delas é o que separa um código funcional de um código eficiente.
BoFactory.loadObjectByParamsAsync(className, params) carrega um Business Object por parâmetros. Use quando precisar buscar dados de outra entidade dentro do .bl.js. Exemplo: dentro do AfterLoadAsync de BoVisit, buscar o Account associado para obter dados complementares.
promise = promise.then(function() {
return BoFactory.loadObjectByParamsAsync('BoAccount', {
pKey: me.getaccountPKey()
});
}).then(function(account) {
if (Utils.isDefined(account)) {
me.setaccountName(account.getname());
}
return context;
});
Facade.getListAsync(loName, jsonQuery) carrega um ListObject com uma query. Use para buscar coleções de dados, como a lista de produtos disponíveis para um pedido.
PKey.next() gera um novo ID. Fundamental dentro do CreateAsync quando você precisa criar registros filhos que dependem do ID do objeto pai.
STATE.NEW / STATE.DIRTY marcam o estado do objeto. Um objeto em STATE.NEW indica que acabou de ser criado. STATE.DIRTY indica que foi modificado mas ainda não salvo. O framework usa esses estados internamente, e você pode verificar para condicionar lógica.
ApplicationContext.log(message) escreve no log do aplicativo. Essencial para depuração em ambiente de simulação. Quando algo não funciona como esperado, os logs são seu primeiro recurso.
Utils.replaceMacrosParam(sql, params) substitui macros em SQL. Útil quando você constrói queries dinâmicas dentro de DataSources scriptados e precisa injetar parâmetros do contexto.
Anti-padrões que causam problemas em produção
Depois de implementar .bl.js em vários projetos, identifiquei padrões que parecem corretos mas causam problemas reais.
Ignorar o assincronismo
O erro mais comum. Desenvolvedores vindos de Apex ou JavaScript síncrono escrevem lógica direta sem considerar que o framework é baseado em promises. O resultado é código que funciona no simulador mas falha no dispositivo porque operações assíncronas ainda não terminaram quando o valor é usado.
Regra: qualquer operação que consulta o banco, carrega outro objeto ou depende de dados que vêm de fonte externa precisa estar dentro de uma cadeia de promise.then().
Lógica pesada no LoadAsync
O AfterLoadAsync executa toda vez que o objeto é carregado. Se você coloca consultas pesadas ali (múltiplas chamadas a BoFactory, loops sobre coleções grandes), o tempo de carregamento do aplicativo vai degradar. Em um ListObject com 200 itens, cada item executando seu AfterLoadAsync com três consultas ao banco gera 600 queries.
Solução: mova lógica pesada para métodos customizados que o usuário invoca explicitamente, ou pré-calcule valores no SaveAsync para que o LoadAsync precise apenas ler.
Esquecer o return promise
Cada função .bl.js precisa retornar uma promise. Se você esquece o return promise no final, o framework não consegue encadear a execução e o comportamento fica imprevisível. Em alguns casos, o processo simplesmente pula a etapa. Em outros, trava silenciosamente.
Modificar o contexto direto
O context passado para a função é compartilhado entre as etapas do processo. Se você modifica propriedades do context diretamente sem intenção, pode afetar etapas posteriores de formas inesperadas. Trabalhe sempre com o Business Object via me e deixe o framework gerenciar o context.
Não verificar com Utils.isDefined()
No ambiente offline do CGC Mobile, dados podem estar ausentes. Uma conta sem endereço, uma visita sem tarefas, um pedido sem itens. Se você acessa propriedades de objetos que podem ser null sem verificar, vai receber erros de runtime que são difíceis de reproduzir porque dependem do estado dos dados no dispositivo.
// Errado
var address = account.getaddress().getcity();
// Correto
var addressObj = account.getaddress();
var city = Utils.isDefined(addressObj) ? addressObj.getcity() : '';
Caso real: validação de pedido com múltiplas regras
Um cenário que aparece em quase todo projeto de CGC: validar um pedido antes do save com múltiplas regras de negócio que dependem de dados de diferentes fontes.
'use strict';
function doValidateAsync(context) {
var me = this;
var errors = [];
var promise = when.resolve(context);
// Regra 1: Pedido deve ter pelo menos um item
promise = promise.then(function() {
var items = me.getorderItems();
if (!Utils.isDefined(items) || items.length === 0) {
errors.push('O pedido deve conter pelo menos um item.');
}
return context;
});
// Regra 2: Verificar crédito disponível do cliente
promise = promise.then(function() {
return BoFactory.loadObjectByParamsAsync('BoAccount', {
pKey: me.getaccountPKey()
});
}).then(function(account) {
if (Utils.isDefined(account)) {
var creditLimit = account.getcreditLimit();
var orderTotal = me.getorderTotal();
if (orderTotal > creditLimit) {
errors.push('O valor do pedido (R$ ' + orderTotal.toFixed(2) +
') excede o limite de crédito do cliente (R$ ' +
creditLimit.toFixed(2) + ').');
}
}
return context;
});
// Regra 3: Data de entrega não pode ser no passado
promise = promise.then(function() {
var deliveryDate = me.getdeliveryDate();
if (Utils.isDefined(deliveryDate)) {
var today = new Date();
today.setHours(0, 0, 0, 0);
if (new Date(deliveryDate) < today) {
errors.push('A data de entrega não pode ser no passado.');
}
}
return context;
});
// Resultado final
promise = promise.then(function() {
if (errors.length > 0) {
return when.reject(errors.join(' '));
}
return context;
});
return promise;
}
Note como cada regra é uma etapa separada na cadeia de promises. Isso mantém o código legível e permite adicionar ou remover regras sem afetar as outras. O array errors acumula todas as violações e o resultado final mostra todas de uma vez ao usuário, em vez de forçá-lo a corrigir uma por uma.
Métodos customizados além do lifecycle
Nem toda lógica pertence a um lifecycle hook. Quando o representante de campo clica em um botão para calcular frete, aplicar desconto ou gerar um documento, o framework invoca um método customizado.
Para criar um método customizado, declare no XML do Business Object:
<Methods>
<Method name="applyDiscount"/>
</Methods>
E crie o arquivo correspondente em Mv2/BoOrder.applyDiscount.bl.js. O processo chama o método via action LOGIC, e o framework invoca a função com o mesmo padrão de promises.
Dicas de performance para ambientes offline
O aplicativo CGC Mobile roda contra SQLite no dispositivo. A performance de queries no SQLite é diferente do que você esperaria no Salesforce. Alguns cuidados fazem diferença real.
Minimize consultas dentro de loops. Se você precisa de dados de 50 produtos, carregue todos de uma vez com getListAsync em vez de 50 chamadas individuais a loadObjectByParamsAsync.
Use DataSource scriptado (external=”true”) para queries complexas em vez de tentar resolver tudo no .bl.js. O Modeler otimiza queries declarativas de formas que código JavaScript não consegue.
Evite criar objetos Business Object desnecessariamente. Cada instanciação consome memória e tempo. Se você só precisa de dois campos de uma entidade, considere usar um ListObject com projeção mínima em vez de carregar o Business Object completo.
Conclusão
A lógica de negócio em .bl.js é o que transforma o Consumer Goods Cloud Modeler de uma ferramenta de configuração em uma plataforma de desenvolvimento real. Os lifecycle hooks dão controle sobre cada momento do ciclo de vida do objeto, o ProcessContext conecta a lógica ao fluxo da aplicação, e as APIs do framework oferecem acesso a toda a stack de dados offline.
O segredo está em entender o modelo assíncrono, respeitar a separação de responsabilidades entre os hooks e evitar a tentação de colocar toda a lógica em um único ponto. Quando você distribui corretamente as responsabilidades entre Initialize, LoadAsync, SaveAsync e ValidateAsync, o código fica mais fácil de manter e o aplicativo performa melhor no dispositivo.
Se você está começando com CGC Modeler, comece com implementações simples no AfterLoadAsync e vá expandindo. E se já tem experiência, revisite seus .bl.js com foco nos anti-padrões que descrevi aqui. Quase todo projeto tem pelo menos um deles escondido em algum lugar.
