SimpleORM Skills
Sistema de plugins para interceptar e enriquecer operacoes CRUD automaticamente.
Introducao
Skills sao unidades de comportamento reutilizaveis que se conectam ao pipeline de operacoes
do TSimpleDAO<T>. Cada Skill implementa a interface iSimpleSkill
e e executada automaticamente antes ou depois de operacoes de Insert, Update ou Delete.
Interface iSimpleSkill
Toda Skill implementa esta interface, declarada em SimpleInterface.pas:
iSimpleSkill = interface
['{GUID}']
function RunAt: TSkillRunAt;
procedure Execute(const aContext: iSimpleSkillContext);
end;
TSkillRunAt
O enum TSkillRunAt define quando a Skill e executada:
| Valor | Descricao |
|---|---|
srBeforeInsert | Antes do INSERT no banco |
srAfterInsert | Apos o INSERT no banco |
srBeforeUpdate | Antes do UPDATE no banco |
srAfterUpdate | Apos o UPDATE no banco |
srBeforeDelete | Antes do DELETE no banco |
srAfterDelete | Apos o DELETE no banco |
API Fluente via .Skill()
Skills sao registradas no DAO usando o metodo fluente .Skill().
Voce pode encadear quantas Skills quiser:
LDAO := TSimpleDAO<TProduto>.New(LQuery)
.Skill(TSkillLog.New('APP', srAfterInsert))
.Skill(TSkillValidate.New)
.Skill(TSkillTimestamp.New('CREATED_AT', srBeforeInsert))
.Skill(TSkillTimestamp.New('UPDATED_AT', srBeforeUpdate));
Skills sao executadas na ordem em que foram registradas. A ordem importa: se uma Skill de validacao lanca excecao, as Skills subsequentes nao serao executadas.
Como Criar uma Skill Customizada
Para criar uma Skill customizada, basta implementar a interface iSimpleSkill
e registrar no DAO via .Skill().
Passo 1: Implementar a Interface
unit SimpleSkillCustom;
interface
uses
SimpleInterface,
SimpleTypes;
type
TSkillCustom = class(TInterfacedObject, iSimpleSkill)
private
FRunAt: TSkillRunAt;
public
class function New(aRunAt: TSkillRunAt): iSimpleSkill;
function RunAt: TSkillRunAt;
procedure Execute(const aContext: iSimpleSkillContext);
end;
implementation
class function TSkillCustom.New(aRunAt: TSkillRunAt): iSimpleSkill;
begin
Result := Self.Create;
TSkillCustom(Result).FRunAt := aRunAt;
end;
function TSkillCustom.RunAt: TSkillRunAt;
begin
Result := FRunAt;
end;
procedure TSkillCustom.Execute(const aContext: iSimpleSkillContext);
begin
// aContext.Entity — objeto da entidade
// aContext.Operation — tipo da operacao (soInsert, soUpdate, soDelete)
// aContext.Query — instancia iSimpleQuery (pode ser nil)
// aContext.Logger — instancia iSimpleQueryLogger (pode ser nil)
// aContext.AIClient — instancia iSimpleAIClient (pode ser nil)
// Sua logica customizada aqui
end;
end.
Passo 2: Registrar no DAO
LDAO := TSimpleDAO<TCliente>.New(LQuery)
.Skill(TSkillCustom.New(srBeforeInsert));
O contexto (iSimpleSkillContext) fornece acesso ao objeto da entidade, a operacao sendo realizada, e a instancia do Query. Use RTTI para acessar propriedades da entidade de forma generica.
Skills Built-in
O SimpleORM inclui 8 Skills prontas para uso, declaradas em SimpleSkill.pas.
Basta instanciar e registrar no DAO.
TSkillLog
Loga operacoes via Logger (se disponivel) ou OutputDebugString (Windows) / Writeln (console).
Construtor
class function New(const aPrefix: String; aRunAt: TSkillRunAt): iSimpleSkill;
Parametros
| Parametro | Tipo | Descricao |
|---|---|---|
aPrefix | String | Prefixo da mensagem de log (ex: nome da aplicacao) |
aRunAt | TSkillRunAt | Momento de execucao (tipicamente srAfterInsert, srAfterUpdate, srAfterDelete) |
Formato da Mensagem
[PREFIX] OPERATION on ENTITY: field1=val1, field2=val2
Exemplo de Uso
LDAO := TSimpleDAO<TCliente>.New(LQuery)
.Skill(TSkillLog.New('VENDAS', srAfterInsert))
.Skill(TSkillLog.New('VENDAS', srAfterUpdate))
.Skill(TSkillLog.New('VENDAS', srAfterDelete));
// Ao inserir um cliente, o log sera:
// [VENDAS] INSERT on CLIENTE: ID=1, NOME=Joao Silva, EMAIL=joao@email.com
Se um iSimpleQueryLogger estiver configurado no DAO via .Logger(), o TSkillLog o utiliza. Caso contrario, usa OutputDebugString no Windows ou Writeln em aplicacoes console.
TSkillNotify
Dispara um callback TProc<TObject> apos uma operacao. Ideal para notificacoes, envio de email, atualizacao de cache, ou qualquer acao customizada.
Construtor
class function New(aCallback: TProc<TObject>; aRunAt: TSkillRunAt): iSimpleSkill;
Parametros
| Parametro | Tipo | Descricao |
|---|---|---|
aCallback | TProc<TObject> | Procedimento anonimo recebendo a entidade como parametro |
aRunAt | TSkillRunAt | Momento de execucao |
Exemplo de Uso
LDAO := TSimpleDAO<TPedido>.New(LQuery)
.Skill(TSkillNotify.New(
procedure(aEntity: TObject)
var
LPedido: TPedido;
begin
LPedido := aEntity as TPedido;
EnviarEmailConfirmacao(LPedido.Email, LPedido.Numero);
end,
srAfterInsert
))
.Skill(TSkillNotify.New(
procedure(aEntity: TObject)
begin
CacheManager.Invalidate('pedidos');
end,
srAfterUpdate
));
O callback e executado de forma sincrona na mesma thread. Se precisar de processamento assincrono, inicie uma thread dentro do callback.
TSkillAudit
Grava um registro de auditoria em uma tabela do banco de dados a cada operacao CRUD. Requer que o iSimpleQuery esteja disponivel no contexto.
Construtor
class function New(const aTableName: String; aRunAt: TSkillRunAt): iSimpleSkill;
Parametros
| Parametro | Tipo | Descricao |
|---|---|---|
aTableName | String | Nome da tabela de auditoria no banco |
aRunAt | TSkillRunAt | Momento de execucao (tipicamente srAfterInsert, srAfterUpdate, srAfterDelete) |
DDL da Tabela de Auditoria
CREATE TABLE AUDIT_LOG (
ID INTEGER PRIMARY KEY,
ENTITY_NAME VARCHAR(100) NOT NULL,
RECORD_ID VARCHAR(50) NOT NULL,
OPERATION VARCHAR(20) NOT NULL,
DETAILS VARCHAR(4000),
CREATED_AT TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Exemplo de Uso
LDAO := TSimpleDAO<TCliente>.New(LQuery)
.Skill(TSkillAudit.New('AUDIT_LOG', srAfterInsert))
.Skill(TSkillAudit.New('AUDIT_LOG', srAfterUpdate))
.Skill(TSkillAudit.New('AUDIT_LOG', srAfterDelete));
// Ao inserir um cliente, sera gravado na AUDIT_LOG:
// ENTITY_NAME = 'CLIENTE'
// RECORD_ID = '42'
// OPERATION = 'INSERT'
// DETAILS = 'NOME=Joao Silva, EMAIL=joao@email.com'
// CREATED_AT = 2026-03-10 14:30:00
A tabela de auditoria DEVE existir no banco antes de usar esta Skill. Caso contrario, o INSERT de auditoria falhara e a excecao sera propagada.
TSkillTimestamp
Preenche automaticamente um campo de data/hora na entidade usando Now. Ideal para campos como CREATED_AT e UPDATED_AT.
Construtor
class function New(const aFieldName: String; aRunAt: TSkillRunAt): iSimpleSkill;
Parametros
| Parametro | Tipo | Descricao |
|---|---|---|
aFieldName | String | Nome do campo [Campo] que sera preenchido |
aRunAt | TSkillRunAt | Momento de execucao |
Exemplo de Uso
LDAO := TSimpleDAO<TProduto>.New(LQuery)
.Skill(TSkillTimestamp.New('CREATED_AT', srBeforeInsert))
.Skill(TSkillTimestamp.New('UPDATED_AT', srBeforeUpdate));
// Entidade:
// [Tabela('PRODUTO')]
// TProduto = class
// published
// [Campo('CREATED_AT')]
// property CriadoEm: TDateTime read FCriadoEm write FCriadoEm;
// [Campo('UPDATED_AT')]
// property AtualizadoEm: TDateTime read FAtualizadoEm write FAtualizadoEm;
// end;
// Ao inserir, CREATED_AT recebe Now automaticamente.
// Ao atualizar, UPDATED_AT recebe Now automaticamente.
O campo e localizado via RTTI pelo nome do atributo [Campo]. Se o campo nao for encontrado na entidade, a Skill e ignorada silenciosamente.
TSkillValidate
Chama TSimpleValidator.Validate automaticamente antes da operacao. Valida todos os atributos de validacao presentes na entidade.
Construtor
class function New(aRunAt: TSkillRunAt = srBeforeInsert): iSimpleSkill;
Parametros
| Parametro | Tipo | Descricao |
|---|---|---|
aRunAt | TSkillRunAt | Momento de execucao (padrao: srBeforeInsert) |
Atributos Validados
| Atributo | Validacao |
|---|---|
[NotNull] | Campo nao pode ser vazio ou nulo |
[NotZero] | Campo numerico nao pode ser zero |
[Format(max, min)] | Tamanho da string deve estar entre min e max |
[Email] | Formato de email valido |
[MinValue(n)] | Valor minimo permitido |
[MaxValue(n)] | Valor maximo permitido |
[Regex('pattern', 'msg')] | Valor deve corresponder ao regex |
[CPF] | CPF valido (com algoritmo de digitos) |
[CNPJ] | CNPJ valido (com algoritmo de digitos) |
Exemplo de Uso
LDAO := TSimpleDAO<TCliente>.New(LQuery)
.Skill(TSkillValidate.New(srBeforeInsert))
.Skill(TSkillValidate.New(srBeforeUpdate));
// Se a validacao falhar, lanca ESimpleValidator com todas as mensagens acumuladas.
// A operacao de INSERT/UPDATE NAO sera executada.
Se a validacao falhar, a excecao ESimpleValidator e lancada e a operacao e interrompida. Trate a excecao no codigo chamador para exibir as mensagens ao usuario.
TSkillGuardDelete
Bloqueia a exclusao de um registro quando existem registros dependentes em uma tabela filha. Previne violacoes de integridade referencial de forma amigavel.
Construtor
class function New(const aChildTable, aFKField: String): iSimpleSkill;
Parametros
| Parametro | Tipo | Descricao |
|---|---|---|
aChildTable | String | Nome da tabela filha que pode ter registros dependentes |
aFKField | String | Nome do campo de FK na tabela filha que referencia a PK da entidade |
O RunAt e fixo em srBeforeDelete. Nao e necessario informar.
Comportamento
Executa a seguinte query antes do DELETE:
SELECT COUNT(*) FROM [aChildTable] WHERE [aFKField] = :pPK
Se o resultado for maior que zero, lanca ESimpleGuardDelete e o DELETE nao e executado.
Exemplo de Uso
LDAO := TSimpleDAO<TCliente>.New(LQuery)
.Skill(TSkillGuardDelete.New('PEDIDO', 'ID_CLIENTE'))
.Skill(TSkillGuardDelete.New('CONTA_RECEBER', 'ID_CLIENTE'));
// Ao tentar deletar um cliente que tem pedidos:
// ESimpleGuardDelete: 'Nao e possivel excluir. Existem registros dependentes em PEDIDO.'
// Voce pode registrar multiplos guards para verificar varias tabelas filhas.
TSkillHistory
Grava um snapshot dos valores da entidade antes de uma alteracao. Cada campo e gravado como um registro separado na tabela de historico.
Construtor
class function New(const aHistoryTable: String; aRunAt: TSkillRunAt): iSimpleSkill;
Parametros
| Parametro | Tipo | Descricao |
|---|---|---|
aHistoryTable | String | Nome da tabela de historico no banco |
aRunAt | TSkillRunAt | Momento de execucao (tipicamente srBeforeUpdate ou srBeforeDelete) |
DDL da Tabela de Historico
CREATE TABLE ENTITY_HISTORY (
ID INTEGER PRIMARY KEY,
ENTITY_NAME VARCHAR(100) NOT NULL,
RECORD_ID VARCHAR(50) NOT NULL,
FIELD_NAME VARCHAR(100) NOT NULL,
OLD_VALUE VARCHAR(4000),
OPERATION VARCHAR(20) NOT NULL,
CREATED_AT TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Exemplo de Uso
LDAO := TSimpleDAO<TCliente>.New(LQuery)
.Skill(TSkillHistory.New('ENTITY_HISTORY', srBeforeUpdate))
.Skill(TSkillHistory.New('ENTITY_HISTORY', srBeforeDelete));
// Ao atualizar o cliente ID=42 (NOME='Joao' -> 'Joao Silva'),
// sera gravado na ENTITY_HISTORY:
//
// ENTITY_NAME = 'CLIENTE'
// RECORD_ID = '42'
// FIELD_NAME = 'NOME'
// OLD_VALUE = 'Joao'
// OPERATION = 'UPDATE'
// CREATED_AT = 2026-03-10 14:30:00
//
// (um registro por campo da entidade)
Para UPDATE, os valores gravados sao os valores ANTES da alteracao (old values). Para DELETE, todos os valores atuais sao gravados como snapshot final.
TSkillWebhook
Envia um HTTP POST fire-and-forget com o JSON da entidade para uma URL configurada. Ideal para integracoes externas, webhooks e notificacoes em tempo real.
Construtor
class function New(const aURL: String; aRunAt: TSkillRunAt; const aAuthHeader: String = ''): iSimpleSkill;
Parametros
| Parametro | Tipo | Descricao |
|---|---|---|
aURL | String | URL de destino do POST |
aRunAt | TSkillRunAt | Momento de execucao |
aAuthHeader | String | Valor do header Authorization (opcional). Ex: 'Bearer token123' |
Exemplo de Uso
LDAO := TSimpleDAO<TPedido>.New(LQuery)
.Skill(TSkillWebhook.New(
'https://api.empresa.com/webhooks/pedido-criado',
srAfterInsert,
'Bearer meu-token-secreto'
))
.Skill(TSkillWebhook.New(
'https://api.empresa.com/webhooks/pedido-cancelado',
srAfterDelete
));
// O JSON enviado segue o formato do TSimpleSerializer:
// {
// "ID_PEDIDO": 42,
// "NUMERO": "PED-2026-001",
// "VALOR_TOTAL": 1500.00,
// "ID_CLIENTE": 7
// }
Se o HTTP POST falhar (timeout, erro de rede, status >= 400), o erro e logado mas NAO bloqueia a operacao CRUD. A entidade sera salva normalmente. Para cenarios onde a falha do webhook deve bloquear a operacao, crie uma Skill customizada.
Skills ERP
Skills especializadas para cenarios de ERP e gestao empresarial, declaradas em SimpleERPSkill.pas.
Inclui validacao de documentos brasileiros (CPF/CNPJ) e skills para numeracao, calculo, estoque e duplicatas.
Atributos CPF e CNPJ
Os atributos [CPF] e [CNPJ] permitem validacao automatica de documentos brasileiros
em properties de entidades. A validacao e executada pelo TSimpleValidator.
Uso em Entidades
[Tabela('CLIENTE')]
TCliente = class
private
FCPF: String;
FCNPJ: String;
// getters e setters...
published
[Campo('CPF')]
[CPF]
property CPF: String read FCPF write FCPF;
[Campo('CNPJ')]
[CNPJ]
property CNPJ: String read FCNPJ write FCNPJ;
end;
Algoritmo de Validacao
A validacao segue o algoritmo oficial da Receita Federal:
| Etapa | CPF | CNPJ |
|---|---|---|
| 1. Strip mascara | Remove . e - | Remove ., / e - |
| 2. Tamanho | Deve ter 11 digitos | Deve ter 14 digitos |
| 3. Todos iguais | Rejeita (ex: 111.111.111-11) | Rejeita (ex: 11.111.111/1111-11) |
| 4. Digitos verificadores | Calcula e compara 2 digitos | Calcula e compara 2 digitos |
Exemplos
// CPF valido (com ou sem mascara):
LCliente.CPF := '529.982.247-25'; // OK
LCliente.CPF := '52998224725'; // OK
// CPF invalido:
LCliente.CPF := '111.111.111-11'; // FALHA: todos digitos iguais
LCliente.CPF := '123.456.789-00'; // FALHA: digitos verificadores incorretos
LCliente.CPF := '123'; // FALHA: tamanho incorreto
// CNPJ valido:
LCliente.CNPJ := '11.222.333/0001-81'; // OK
LCliente.CNPJ := '11222333000181'; // OK
// CNPJ invalido:
LCliente.CNPJ := '11.111.111/1111-11'; // FALHA: todos digitos iguais
LCliente.CNPJ := '00.000.000/0000-00'; // FALHA: todos digitos iguais
A validacao aceita o documento com ou sem mascara. A mascara e removida automaticamente antes do calculo dos digitos verificadores.
TSkillSequence
Gera numeracao sequencial automatica para campos como numero de pedido, numero de nota fiscal, etc. Utiliza uma tabela de controle no banco para manter o ultimo numero gerado por serie.
Construtor
class function New(const aTargetField, aControlTable, aSeries: String): iSimpleSkill;
Parametros
| Parametro | Tipo | Descricao |
|---|---|---|
aTargetField | String | Nome do campo [Campo] que recebera o numero gerado |
aControlTable | String | Nome da tabela de controle de numeracao |
aSeries | String | Identificador da serie (permite multiplas sequencias independentes) |
O RunAt e fixo em srBeforeInsert. Nao e necessario informar.
DDL da Tabela de Controle
CREATE TABLE NUMERACAO (
ID INTEGER PRIMARY KEY,
SERIE VARCHAR(50) NOT NULL UNIQUE,
ULTIMO_NUMERO INTEGER NOT NULL DEFAULT 0
);
-- Inserir a serie antes do primeiro uso:
INSERT INTO NUMERACAO (ID, SERIE, ULTIMO_NUMERO) VALUES (1, 'PEDIDO', 0);
INSERT INTO NUMERACAO (ID, SERIE, ULTIMO_NUMERO) VALUES (2, 'NF', 0);
Comportamento
A cada execucao:
- Faz
SELECT ULTIMO_NUMERO FROM [tabela] WHERE SERIE = :pSerie - Incrementa o valor em 1
- Faz
UPDATE [tabela] SET ULTIMO_NUMERO = :pNumero WHERE SERIE = :pSerie - Atribui o novo numero ao campo da entidade via RTTI
Exemplo Completo
[Tabela('PEDIDO')]
TPedido = class
private
FId: Integer;
FNumero: Integer;
FIdCliente: Integer;
FValorTotal: Currency;
// getters e setters...
published
[Campo('ID_PEDIDO')][PK][AutoInc]
property Id: Integer read FId write FId;
[Campo('NUMERO')]
property Numero: Integer read FNumero write FNumero;
[Campo('ID_CLIENTE')][FK]
property IdCliente: Integer read FIdCliente write FIdCliente;
[Campo('VALOR_TOTAL')]
property ValorTotal: Currency read FValorTotal write FValorTotal;
end;
// Configuracao do DAO:
LDAO := TSimpleDAO<TPedido>.New(LQuery)
.Skill(TSkillSequence.New('NUMERO', 'NUMERACAO', 'PEDIDO'));
// Ao inserir:
LPedido := TPedido.Create;
try
LPedido.IdCliente := 42;
LPedido.ValorTotal := 1500.00;
// Nao precisa setar Numero — sera preenchido automaticamente!
LDAO.Insert(LPedido);
Writeln('Pedido criado com numero: ', LPedido.Numero);
finally
LPedido.Free;
end;
TSkillCalcTotal
Calcula automaticamente o valor total de um item baseado em quantidade, preco unitario e desconto. Ideal para itens de pedido, orcamentos e notas fiscais.
Construtor
class function New(const aTotalField, aQtyField, aPriceField, aDiscountField: String): iSimpleSkill;
Parametros
| Parametro | Tipo | Descricao |
|---|---|---|
aTotalField | String | Nome do campo [Campo] que recebera o total calculado |
aQtyField | String | Nome do campo [Campo] de quantidade |
aPriceField | String | Nome do campo [Campo] de preco unitario |
aDiscountField | String | Nome do campo [Campo] de desconto |
Formula
Total = (Quantidade * Preco) - Desconto
O resultado e arredondado para 2 casas decimais usando SimpleRoundTo.
O RunAt e executado em srBeforeInsert E srBeforeUpdate automaticamente. Nao e necessario registrar duas vezes.
Exemplo Completo
[Tabela('ITEM_PEDIDO')]
TItemPedido = class
private
FId: Integer;
FIdPedido: Integer;
FIdProduto: Integer;
FQuantidade: Double;
FPrecoUnitario: Currency;
FDesconto: Currency;
FValorTotal: Currency;
// getters e setters...
published
[Campo('ID_ITEM')][PK][AutoInc]
property Id: Integer read FId write FId;
[Campo('ID_PEDIDO')][FK]
property IdPedido: Integer read FIdPedido write FIdPedido;
[Campo('ID_PRODUTO')][FK]
property IdProduto: Integer read FIdProduto write FIdProduto;
[Campo('QUANTIDADE')]
property Quantidade: Double read FQuantidade write FQuantidade;
[Campo('PRECO_UNITARIO')]
property PrecoUnitario: Currency read FPrecoUnitario write FPrecoUnitario;
[Campo('DESCONTO')]
property Desconto: Currency read FDesconto write FDesconto;
[Campo('VALOR_TOTAL')]
property ValorTotal: Currency read FValorTotal write FValorTotal;
end;
// Configuracao do DAO:
LDAO := TSimpleDAO<TItemPedido>.New(LQuery)
.Skill(TSkillCalcTotal.New('VALOR_TOTAL', 'QUANTIDADE', 'PRECO_UNITARIO', 'DESCONTO'));
// Ao inserir:
LItem := TItemPedido.Create;
try
LItem.IdPedido := 1;
LItem.IdProduto := 10;
LItem.Quantidade := 3;
LItem.PrecoUnitario := 49.90;
LItem.Desconto := 10.00;
// Nao precisa setar ValorTotal — sera calculado automaticamente!
// ValorTotal = (3 * 49.90) - 10.00 = 139.70
LDAO.Insert(LItem);
Writeln('Total calculado: ', CurrToStr(LItem.ValorTotal));
finally
LItem.Free;
end;
TSkillStockMove
Registra automaticamente movimentacoes de estoque ao inserir ou deletar registros. Grava entradas e saidas em uma tabela de movimentacao.
Construtor
class function New(const aStockTable, aProductField, aQtyField: String; aRunAt: TSkillRunAt): iSimpleSkill;
Parametros
| Parametro | Tipo | Descricao |
|---|---|---|
aStockTable | String | Nome da tabela de movimentacao de estoque |
aProductField | String | Nome do campo [Campo] que identifica o produto |
aQtyField | String | Nome do campo [Campo] de quantidade |
aRunAt | TSkillRunAt | Momento de execucao |
Comportamento
| Operacao | Tipo Gravado | Descricao |
|---|---|---|
| INSERT | 'ENTRADA' | Registro de entrada de estoque |
| DELETE | 'SAIDA' | Registro de saida de estoque |
DDL da Tabela de Movimentacao
CREATE TABLE MOV_ESTOQUE (
ID INTEGER PRIMARY KEY,
ID_PRODUTO INTEGER NOT NULL,
QUANTIDADE DOUBLE PRECISION NOT NULL,
TIPO VARCHAR(20) NOT NULL,
CREATED_AT TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Exemplo Completo
[Tabela('COMPRA_ITEM')]
TCompraItem = class
private
FId: Integer;
FIdProduto: Integer;
FQuantidade: Double;
// getters e setters...
published
[Campo('ID_ITEM')][PK][AutoInc]
property Id: Integer read FId write FId;
[Campo('ID_PRODUTO')][FK]
property IdProduto: Integer read FIdProduto write FIdProduto;
[Campo('QUANTIDADE')]
property Quantidade: Double read FQuantidade write FQuantidade;
end;
// Configuracao do DAO:
LDAO := TSimpleDAO<TCompraItem>.New(LQuery)
.Skill(TSkillStockMove.New('MOV_ESTOQUE', 'ID_PRODUTO', 'QUANTIDADE', srAfterInsert))
.Skill(TSkillStockMove.New('MOV_ESTOQUE', 'ID_PRODUTO', 'QUANTIDADE', srBeforeDelete));
// Ao inserir um item de compra (Produto 10, Qty 50):
// MOV_ESTOQUE recebe: ID_PRODUTO=10, QUANTIDADE=50, TIPO='ENTRADA'
// Ao deletar o item:
// MOV_ESTOQUE recebe: ID_PRODUTO=10, QUANTIDADE=50, TIPO='SAIDA'
TSkillDuplicate
Gera automaticamente parcelas financeiras (duplicatas) apos a insercao de um registro. Divide o valor total em N parcelas com vencimentos sequenciais.
Construtor
class function New(const aInstallmentTable, aTotalField: String; aInstallments, aIntervalDays: Integer): iSimpleSkill;
Parametros
| Parametro | Tipo | Descricao |
|---|---|---|
aInstallmentTable | String | Nome da tabela de parcelas |
aTotalField | String | Nome do campo [Campo] com o valor total a ser parcelado |
aInstallments | Integer | Numero de parcelas |
aIntervalDays | Integer | Intervalo em dias entre vencimentos |
O RunAt e fixo em srAfterInsert. Nao e necessario informar.
DDL da Tabela de Parcelas
CREATE TABLE PARCELA (
ID INTEGER PRIMARY KEY,
ID_ORIGEM INTEGER NOT NULL,
ENTITY_NAME VARCHAR(100) NOT NULL,
NUMERO_PARCELA INTEGER NOT NULL,
VALOR DECIMAL(15,2) NOT NULL,
VENCIMENTO DATE NOT NULL,
STATUS VARCHAR(20) DEFAULT 'ABERTA'
);
Comportamento
Ao inserir, a Skill:
- Le o valor total do campo especificado via RTTI
- Divide o valor por N parcelas
- Calcula o vencimento de cada parcela:
Now + (intervalo * numero_parcela) - Insere N registros na tabela de parcelas
Exemplo Completo
[Tabela('VENDA')]
TVenda = class
private
FId: Integer;
FIdCliente: Integer;
FValorTotal: Currency;
// getters e setters...
published
[Campo('ID_VENDA')][PK][AutoInc]
property Id: Integer read FId write FId;
[Campo('ID_CLIENTE')][FK]
property IdCliente: Integer read FIdCliente write FIdCliente;
[Campo('VALOR_TOTAL')]
property ValorTotal: Currency read FValorTotal write FValorTotal;
end;
// Configuracao do DAO — 3 parcelas a cada 30 dias:
LDAO := TSimpleDAO<TVenda>.New(LQuery)
.Skill(TSkillDuplicate.New('PARCELA', 'VALOR_TOTAL', 3, 30));
// Ao inserir uma venda de R$ 900.00:
LVenda := TVenda.Create;
try
LVenda.IdCliente := 42;
LVenda.ValorTotal := 900.00;
LDAO.Insert(LVenda);
finally
LVenda.Free;
end;
// Resultado na tabela PARCELA:
// | NUMERO_PARCELA | VALOR | VENCIMENTO | STATUS |
// |----------------|--------|-------------|--------|
// | 1 | 300.00 | 2026-04-09 | ABERTA |
// | 2 | 300.00 | 2026-05-09 | ABERTA |
// | 3 | 300.00 | 2026-06-08 | ABERTA |
Se o valor total nao for divisivel igualmente, a diferenca de centavos e adicionada a ultima parcela para garantir que a soma das parcelas seja exatamente igual ao valor total.
Skills AI
Skills de inteligencia artificial declaradas em SimpleAISkill.pas.
Todas requerem uma instancia de iSimpleAIClient no contexto do DAO.
Skills de enriquecimento e analise (TSkillAIEnrich, TSkillAITranslate, TSkillAISummarize, TSkillAITags, TSkillAISentiment) ignoram silenciosamente se AIClient = nil.
Skills de validacao e moderacao (TSkillAIModerate, TSkillAIValidate) lancam ESimpleAIModeration se AIClient = nil, pois sao consideradas criticas para a integridade dos dados.
TSkillAIEnrich
Gera conteudo automaticamente via LLM usando um prompt template. O resultado e atribuido a um campo da entidade.
Construtor
class function New(const aTargetField, aPromptTemplate: String; aRunAt: TSkillRunAt = srBeforeInsert): iSimpleSkill;
Parametros
| Parametro | Tipo | Descricao |
|---|---|---|
aTargetField | String | Nome do campo [Campo] que recebera o texto gerado |
aPromptTemplate | String | Template do prompt com placeholders {PropertyName} |
aRunAt | TSkillRunAt | Momento de execucao (padrao: srBeforeInsert) |
Template de Prompt
O template suporta placeholders no formato {NomeDaProperty}. Cada placeholder e substituido pelo valor atual da property da entidade via RTTI antes de enviar ao LLM.
Exemplo: Gerar Descricao Comercial
LDAO := TSimpleDAO<TProduto>.New(LQuery)
.Skill(TSkillAIEnrich.New(
'DESCRICAO_COMERCIAL',
'Crie uma descricao comercial atraente para o produto "{Nome}" ' +
'da categoria "{Categoria}" com preco R$ {Preco}. ' +
'Maximo 200 caracteres.',
srBeforeInsert
));
// Ao inserir um produto com Nome='Camiseta Polo', Categoria='Vestuario', Preco=89.90:
// O LLM gera algo como: "Camiseta Polo premium em algodao egipcio. Conforto e
// elegancia para o dia a dia. Caimento perfeito, acabamento impecavel."
Exemplo: Gerar Biografia
LDAO := TSimpleDAO<TUsuario>.New(LQuery)
.Skill(TSkillAIEnrich.New(
'BIO',
'Crie uma breve biografia profissional para {Nome}, ' +
'que trabalha como {Cargo} na area de {Departamento}. ' +
'Tom profissional, maximo 150 caracteres.',
srBeforeInsert
));
Exemplo com Multiplos Placeholders
LDAO := TSimpleDAO<TImovel>.New(LQuery)
.Skill(TSkillAIEnrich.New(
'ANUNCIO',
'Crie um texto de anuncio para um imovel: {TipoImovel} com ' +
'{Quartos} quartos, {AreaM2}m2, no bairro {Bairro}, cidade {Cidade}. ' +
'Destaque os pontos fortes. Maximo 300 caracteres.',
srBeforeInsert
));
Se o AIClient nao estiver configurado (nil), a Skill e ignorada e o campo alvo nao e preenchido.
TSkillAITranslate
Traduz automaticamente o conteudo de um campo para outro idioma, gravando o resultado em um campo diferente da entidade.
Construtor
class function New(const aSourceField, aTargetField, aTargetLanguage: String; aRunAt: TSkillRunAt = srBeforeInsert): iSimpleSkill;
Parametros
| Parametro | Tipo | Descricao |
|---|---|---|
aSourceField | String | Nome do campo [Campo] de origem (texto a traduzir) |
aTargetField | String | Nome do campo [Campo] de destino (traducao) |
aTargetLanguage | String | Idioma de destino (ex: 'English', 'Spanish') |
aRunAt | TSkillRunAt | Momento de execucao (padrao: srBeforeInsert) |
Prompt Interno
Translate the following text to {Language}. Return only the translation, nothing else.
Exemplo: Traducao para Ingles e Espanhol
LDAO := TSimpleDAO<TProduto>.New(LQuery)
.Skill(TSkillAITranslate.New('DESCRICAO', 'DESCRICAO_EN', 'English', srBeforeInsert))
.Skill(TSkillAITranslate.New('DESCRICAO', 'DESCRICAO_ES', 'Spanish', srBeforeInsert));
// Ao inserir um produto com DESCRICAO='Camiseta de algodao premium':
// DESCRICAO_EN = 'Premium cotton t-shirt'
// DESCRICAO_ES = 'Camiseta de algodon premium'
Exemplo: Traducao Antes de Update
LDAO := TSimpleDAO<TArtigo>.New(LQuery)
.Skill(TSkillAITranslate.New('TITULO', 'TITULO_EN', 'English', srBeforeUpdate));
// A traducao e refeita a cada atualizacao do artigo.
Se o campo de origem estiver vazio, a Skill e ignorada e o campo de destino nao e alterado.
TSkillAISummarize
Gera automaticamente um resumo de um texto longo, gravando o resultado em outro campo da entidade.
Construtor
class function New(const aSourceField, aTargetField: String; aMaxLength: Integer = 0; aRunAt: TSkillRunAt = srBeforeInsert): iSimpleSkill;
Parametros
| Parametro | Tipo | Descricao |
|---|---|---|
aSourceField | String | Nome do campo [Campo] com o texto a resumir |
aTargetField | String | Nome do campo [Campo] que recebera o resumo |
aMaxLength | Integer | Limite maximo de caracteres do resumo (0 = sem limite) |
aRunAt | TSkillRunAt | Momento de execucao (padrao: srBeforeInsert) |
Prompt Interno
// Quando aMaxLength > 0:
Summarize the following text in at most N characters. Return only the summary, nothing else.
// Quando aMaxLength = 0:
Summarize the following text concisely. Return only the summary, nothing else.
Exemplo: Resumo para Listagem
LDAO := TSimpleDAO<TArtigo>.New(LQuery)
.Skill(TSkillAISummarize.New('CONTEUDO', 'RESUMO', 200, srBeforeInsert));
// Ao inserir um artigo com CONTEUDO de 5000 caracteres:
// RESUMO recebe um texto de no maximo 200 caracteres.
Exemplo: Resumo para SEO
LDAO := TSimpleDAO<TPagina>.New(LQuery)
.Skill(TSkillAISummarize.New('CONTEUDO', 'META_DESCRIPTION', 160, srBeforeInsert))
.Skill(TSkillAISummarize.New('CONTEUDO', 'META_DESCRIPTION', 160, srBeforeUpdate));
// Meta description para SEO, limitada a 160 caracteres.
TSkillAIModerate
Modera conteudo usando o LLM. Se o conteudo for rejeitado, lanca excecao e bloqueia a operacao. Esta e uma Skill critica — se AIClient = nil, lanca ESimpleAIModeration.
Construtor
class function New(const aField: String; const aPolicy: String = ''; aRunAt: TSkillRunAt = srBeforeInsert): iSimpleSkill;
Parametros
| Parametro | Tipo | Descricao |
|---|---|---|
aField | String | Nome do campo [Campo] a ser moderado |
aPolicy | String | Politica de moderacao customizada (opcional). Se vazio, usa moderacao geral |
aRunAt | TSkillRunAt | Momento de execucao (padrao: srBeforeInsert) |
Comportamento
O LLM responde em um dos formatos:
APPROVED— conteudo aprovado, operacao continua normalmenteREJECTED: motivo— conteudo rejeitado, lancaESimpleAIModerationcom o motivo
Exemplo: Moderacao Geral
LDAO := TSimpleDAO<TComentario>.New(LQuery)
.Skill(TSkillAIModerate.New('TEXTO', '', srBeforeInsert));
// Se o comentario contiver conteudo ofensivo, a insercao e bloqueada.
Exemplo: Moderacao com Politica Customizada
LDAO := TSimpleDAO<TReview>.New(LQuery)
.Skill(TSkillAIModerate.New(
'CONTEUDO',
'Reject content that contains: spam links, competitor mentions, ' +
'personal attacks, or fake reviews. Allow constructive criticism.',
srBeforeInsert
));
Exemplo: Tratamento de Rejeicao
try
LDAO.Insert(LComentario);
except
on E: ESimpleAIModeration do
begin
Writeln('Conteudo rejeitado: ' + E.Message);
// Exibir mensagem ao usuario ou logar a tentativa
end;
end;
Se AIClient = nil, esta Skill lanca ESimpleAIModeration imediatamente. Diferente das Skills de enriquecimento, a moderacao e considerada critica e NAO pode ser ignorada.
TSkillAIValidate
Valida dados da entidade usando uma regra expressa em linguagem natural. O LLM analisa TODOS os campos da entidade e decide se os dados sao validos. Skill critica — lanca excecao se AIClient = nil.
Construtor
class function New(const aRule: String; const aErrorMessage: String = ''; aRunAt: TSkillRunAt = srBeforeInsert): iSimpleSkill;
Parametros
| Parametro | Tipo | Descricao |
|---|---|---|
aRule | String | Regra de validacao em linguagem natural |
aErrorMessage | String | Mensagem de erro customizada (opcional). Se vazio, usa o motivo retornado pelo LLM |
aRunAt | TSkillRunAt | Momento de execucao (padrao: srBeforeInsert) |
Comportamento
A Skill envia todos os campos da entidade (extraidos via [Campo] RTTI) ao LLM junto com a regra. O LLM responde:
VALID— dados validos, operacao continuaINVALID: motivo— dados invalidos, lancaESimpleAIModeration
Exemplo: Validar Coerencia de Preco
LDAO := TSimpleDAO<TProduto>.New(LQuery)
.Skill(TSkillAIValidate.New(
'O preco de venda deve ser maior que o preco de custo. ' +
'A margem de lucro deve ser de pelo menos 10%.',
'', // usa motivo do LLM como mensagem de erro
srBeforeInsert
));
Exemplo: Validar Endereco Completo
LDAO := TSimpleDAO<TCliente>.New(LQuery)
.Skill(TSkillAIValidate.New(
'O endereco deve conter rua, numero, cidade e estado. ' +
'O CEP deve ter formato valido (XXXXX-XXX). ' +
'O estado deve ser uma sigla brasileira valida (ex: SP, RJ, MG).',
'',
srBeforeInsert
));
Exemplo: Mensagem de Erro Customizada
LDAO := TSimpleDAO<TPedido>.New(LQuery)
.Skill(TSkillAIValidate.New(
'O valor total do pedido nao pode exceder o limite de credito do cliente.',
'Pedido excede o limite de credito disponivel. Consulte o gerente.',
srBeforeInsert
));
// Se invalido, a mensagem de erro sera sempre:
// 'Pedido excede o limite de credito disponivel. Consulte o gerente.'
// (independente do motivo retornado pelo LLM)
Se AIClient = nil, esta Skill lanca ESimpleAIModeration imediatamente. A validacao por IA e considerada critica.
TSkillAISentiment
Analisa o sentimento de um texto e grava o resultado (POSITIVO, NEGATIVO ou NEUTRO) em outro campo da entidade.
Construtor
class function New(const aSourceField, aTargetField: String; aRunAt: TSkillRunAt = srBeforeInsert): iSimpleSkill;
Parametros
| Parametro | Tipo | Descricao |
|---|---|---|
aSourceField | String | Nome do campo [Campo] com o texto a analisar |
aTargetField | String | Nome do campo [Campo] que recebera o sentimento |
aRunAt | TSkillRunAt | Momento de execucao (padrao: srBeforeInsert) |
Valores Possiveis
| Valor | Descricao |
|---|---|
POSITIVO | Texto com sentimento positivo |
NEGATIVO | Texto com sentimento negativo |
NEUTRO | Texto sem sentimento claro ou informativo |
Exemplo: Analise de Comentario
LDAO := TSimpleDAO<TComentario>.New(LQuery)
.Skill(TSkillAISentiment.New('TEXTO', 'SENTIMENTO', srBeforeInsert));
// Ao inserir um comentario 'Produto excelente! Entrega rapida!':
// SENTIMENTO = 'POSITIVO'
// Ao inserir 'Produto veio com defeito, pessimo atendimento':
// SENTIMENTO = 'NEGATIVO'
// Ao inserir 'Recebi o produto ontem':
// SENTIMENTO = 'NEUTRO'
Exemplo: Filtrar Reviews por Sentimento
LDAO := TSimpleDAO<TReview>.New(LQuery)
.Skill(TSkillAISentiment.New('CONTEUDO', 'SENTIMENTO', srBeforeInsert));
// Depois, para buscar apenas reviews positivas:
LLista := TObjectList<TReview>.Create;
try
TSimpleDAO<TReview>.New(LQuery)
.SQL
.Where('SENTIMENTO = :pSentimento')
.&End
.Find(LLista);
finally
LLista.Free;
end;
Se AIClient = nil, a Skill e ignorada silenciosamente e o campo de sentimento nao e preenchido. Diferente de TSkillAIModerate e TSkillAIValidate, a analise de sentimento NAO e considerada critica.
Exemplo Completo
Este exemplo demonstra um pipeline completo combinando Skills Built-in, ERP e AI numa unica configuracao de DAO para uma entidade de Pedido.
program ExemploSkillsCompleto;
{$APPTYPE CONSOLE}
uses
SimpleDAO,
SimpleInterface,
SimpleSkill,
SimpleERPSkill,
SimpleAISkill,
SimpleQueryFiredac,
System.SysUtils,
System.Generics.Collections;
var
LQuery: iSimpleQuery;
LDAO: iSimpleDAO<TPedido>;
LPedido: TPedido;
begin
LQuery := TSimpleQueryFiredac.New(FDConnection1);
LDAO := TSimpleDAO<TPedido>.New(LQuery)
// === Skills Built-in ===
// Validacao automatica de atributos
.Skill(TSkillValidate.New(srBeforeInsert))
.Skill(TSkillValidate.New(srBeforeUpdate))
// Timestamp automatico
.Skill(TSkillTimestamp.New('CREATED_AT', srBeforeInsert))
.Skill(TSkillTimestamp.New('UPDATED_AT', srBeforeUpdate))
// Log de operacoes
.Skill(TSkillLog.New('ERP', srAfterInsert))
.Skill(TSkillLog.New('ERP', srAfterUpdate))
.Skill(TSkillLog.New('ERP', srAfterDelete))
// Auditoria no banco
.Skill(TSkillAudit.New('AUDIT_LOG', srAfterInsert))
.Skill(TSkillAudit.New('AUDIT_LOG', srAfterUpdate))
.Skill(TSkillAudit.New('AUDIT_LOG', srAfterDelete))
// Historico antes de alteracoes
.Skill(TSkillHistory.New('ENTITY_HISTORY', srBeforeUpdate))
.Skill(TSkillHistory.New('ENTITY_HISTORY', srBeforeDelete))
// Guard delete — nao deletar pedido com itens
.Skill(TSkillGuardDelete.New('ITEM_PEDIDO', 'ID_PEDIDO'))
.Skill(TSkillGuardDelete.New('PARCELA', 'ID_ORIGEM'))
// Webhook para sistema externo
.Skill(TSkillWebhook.New(
'https://api.empresa.com/webhooks/pedido',
srAfterInsert,
'Bearer token-secreto'
))
// Notificacao customizada
.Skill(TSkillNotify.New(
procedure(aEntity: TObject)
begin
Writeln('Pedido processado com sucesso!');
end,
srAfterInsert
))
// === Skills ERP ===
// Numeracao sequencial automatica
.Skill(TSkillSequence.New('NUMERO', 'NUMERACAO', 'PEDIDO'))
// Geracao de parcelas (3x a cada 30 dias)
.Skill(TSkillDuplicate.New('PARCELA', 'VALOR_TOTAL', 3, 30))
// === Skills AI ===
// Gerar observacao automatica
.Skill(TSkillAIEnrich.New(
'OBSERVACAO',
'Gere uma observacao interna sobre o pedido #{Numero} do cliente {NomeCliente} ' +
'no valor de R$ {ValorTotal}. Maximo 100 caracteres.',
srAfterInsert
))
// Validacao de coerencia por IA
.Skill(TSkillAIValidate.New(
'O valor total deve ser compativel com a quantidade de itens. ' +
'Pedidos acima de R$ 50.000 requerem aprovacao.',
'Pedido requer aprovacao gerencial.',
srBeforeInsert
));
// Usar o DAO normalmente:
LPedido := TPedido.Create;
try
LPedido.IdCliente := 42;
LPedido.NomeCliente := 'Joao Silva';
LPedido.ValorTotal := 1500.00;
// Numero sera gerado pelo TSkillSequence
// CREATED_AT sera preenchido pelo TSkillTimestamp
// Validacao, auditoria, log, webhook, parcelas — tudo automatico!
LDAO.Insert(LPedido);
Writeln('Pedido #', LPedido.Numero, ' criado com sucesso!');
finally
LPedido.Free;
end;
Readln;
end.
A ordem de registro das Skills importa. Skills de validacao devem vir primeiro (antes do insert), Skills de enriquecimento no meio, e Skills de notificacao/log por ultimo (apos o insert). Skills srBefore* sao executadas antes da operacao SQL, Skills srAfter* apos.
Exceptions
Tabela de exceptions lancadas pelas Skills e quando elas ocorrem.
| Exception | Lancada por | Quando |
|---|---|---|
ESimpleValidator |
TSkillValidate |
Quando qualquer atributo de validacao falha ([NotNull], [NotZero], [Format], [Email], [MinValue], [MaxValue], [Regex], [CPF], [CNPJ]). A mensagem contem todas as falhas acumuladas. |
ESimpleGuardDelete |
TSkillGuardDelete |
Quando existem registros dependentes na tabela filha. Mensagem: 'Nao e possivel excluir. Existem registros dependentes em [TABELA].' |
ESimpleAIModeration |
TSkillAIModerate |
Quando o conteudo e rejeitado pelo LLM. Mensagem contem o motivo da rejeicao. Tambem lancada quando AIClient = nil. |
ESimpleAIModeration |
TSkillAIValidate |
Quando os dados sao considerados invalidos pelo LLM. Mensagem contem o motivo ou a aErrorMessage customizada. Tambem lancada quando AIClient = nil. |
Tratamento de Exceptions
try
LDAO.Insert(LEntidade);
except
on E: ESimpleValidator do
Writeln('Validacao falhou: ' + E.Message);
on E: ESimpleGuardDelete do
Writeln('Exclusao bloqueada: ' + E.Message);
on E: ESimpleAIModeration do
Writeln('IA rejeitou: ' + E.Message);
end;
Exceptions lancadas por Skills srBefore* interrompem a operacao CRUD — o INSERT/UPDATE/DELETE NAO sera executado. Exceptions em Skills srAfter* ocorrem apos a operacao ja ter sido gravada no banco.