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:

ValorDescricao
srBeforeInsertAntes do INSERT no banco
srAfterInsertApos o INSERT no banco
srBeforeUpdateAntes do UPDATE no banco
srAfterUpdateApos o UPDATE no banco
srBeforeDeleteAntes do DELETE no banco
srAfterDeleteApos 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

ParametroTipoDescricao
aPrefixStringPrefixo da mensagem de log (ex: nome da aplicacao)
aRunAtTSkillRunAtMomento 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

ParametroTipoDescricao
aCallbackTProc<TObject>Procedimento anonimo recebendo a entidade como parametro
aRunAtTSkillRunAtMomento 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

ParametroTipoDescricao
aTableNameStringNome da tabela de auditoria no banco
aRunAtTSkillRunAtMomento 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

ParametroTipoDescricao
aFieldNameStringNome do campo [Campo] que sera preenchido
aRunAtTSkillRunAtMomento 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

ParametroTipoDescricao
aRunAtTSkillRunAtMomento de execucao (padrao: srBeforeInsert)

Atributos Validados

AtributoValidacao
[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

ParametroTipoDescricao
aChildTableStringNome da tabela filha que pode ter registros dependentes
aFKFieldStringNome 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

ParametroTipoDescricao
aHistoryTableStringNome da tabela de historico no banco
aRunAtTSkillRunAtMomento 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

ParametroTipoDescricao
aURLStringURL de destino do POST
aRunAtTSkillRunAtMomento de execucao
aAuthHeaderStringValor 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:

EtapaCPFCNPJ
1. Strip mascaraRemove . e -Remove ., / e -
2. TamanhoDeve ter 11 digitosDeve ter 14 digitos
3. Todos iguaisRejeita (ex: 111.111.111-11)Rejeita (ex: 11.111.111/1111-11)
4. Digitos verificadoresCalcula e compara 2 digitosCalcula 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

ParametroTipoDescricao
aTargetFieldStringNome do campo [Campo] que recebera o numero gerado
aControlTableStringNome da tabela de controle de numeracao
aSeriesStringIdentificador 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:

  1. Faz SELECT ULTIMO_NUMERO FROM [tabela] WHERE SERIE = :pSerie
  2. Incrementa o valor em 1
  3. Faz UPDATE [tabela] SET ULTIMO_NUMERO = :pNumero WHERE SERIE = :pSerie
  4. 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

ParametroTipoDescricao
aTotalFieldStringNome do campo [Campo] que recebera o total calculado
aQtyFieldStringNome do campo [Campo] de quantidade
aPriceFieldStringNome do campo [Campo] de preco unitario
aDiscountFieldStringNome 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

ParametroTipoDescricao
aStockTableStringNome da tabela de movimentacao de estoque
aProductFieldStringNome do campo [Campo] que identifica o produto
aQtyFieldStringNome do campo [Campo] de quantidade
aRunAtTSkillRunAtMomento de execucao

Comportamento

OperacaoTipo GravadoDescricao
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

ParametroTipoDescricao
aInstallmentTableStringNome da tabela de parcelas
aTotalFieldStringNome do campo [Campo] com o valor total a ser parcelado
aInstallmentsIntegerNumero de parcelas
aIntervalDaysIntegerIntervalo 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:

  1. Le o valor total do campo especificado via RTTI
  2. Divide o valor por N parcelas
  3. Calcula o vencimento de cada parcela: Now + (intervalo * numero_parcela)
  4. 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

ParametroTipoDescricao
aTargetFieldStringNome do campo [Campo] que recebera o texto gerado
aPromptTemplateStringTemplate do prompt com placeholders {PropertyName}
aRunAtTSkillRunAtMomento 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

ParametroTipoDescricao
aSourceFieldStringNome do campo [Campo] de origem (texto a traduzir)
aTargetFieldStringNome do campo [Campo] de destino (traducao)
aTargetLanguageStringIdioma de destino (ex: 'English', 'Spanish')
aRunAtTSkillRunAtMomento 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

ParametroTipoDescricao
aSourceFieldStringNome do campo [Campo] com o texto a resumir
aTargetFieldStringNome do campo [Campo] que recebera o resumo
aMaxLengthIntegerLimite maximo de caracteres do resumo (0 = sem limite)
aRunAtTSkillRunAtMomento 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.

TSkillAITags

Extrai automaticamente tags/keywords de um texto, gravando como string separada por virgulas em outro campo.

Construtor

class function New(const aSourceField, aTargetField: String; aMaxTags: Integer = 5; aRunAt: TSkillRunAt = srBeforeInsert): iSimpleSkill;

Parametros

ParametroTipoDescricao
aSourceFieldStringNome do campo [Campo] com o texto fonte
aTargetFieldStringNome do campo [Campo] que recebera as tags
aMaxTagsIntegerNumero maximo de tags a gerar (padrao: 5)
aRunAtTSkillRunAtMomento de execucao (padrao: srBeforeInsert)

Prompt Interno

Extract at most N keywords from the following text. Return them as a comma-separated list, nothing else.

Exemplo: Tags para Busca

LDAO := TSimpleDAO<TProduto>.New(LQuery)
  .Skill(TSkillAITags.New('DESCRICAO', 'TAGS', 5, srBeforeInsert));

// Ao inserir um produto com DESCRICAO='Camiseta polo masculina azul em algodao':
// TAGS = 'camiseta, polo, masculina, azul, algodao'

Exemplo: Tags para Categorizacao

LDAO := TSimpleDAO<TArtigo>.New(LQuery)
  .Skill(TSkillAITags.New('CONTEUDO', 'KEYWORDS', 10, srBeforeInsert))
  .Skill(TSkillAITags.New('CONTEUDO', 'KEYWORDS', 10, srBeforeUpdate));

// Regenera keywords a cada atualizacao do conteudo.

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

ParametroTipoDescricao
aFieldStringNome do campo [Campo] a ser moderado
aPolicyStringPolitica de moderacao customizada (opcional). Se vazio, usa moderacao geral
aRunAtTSkillRunAtMomento de execucao (padrao: srBeforeInsert)

Comportamento

O LLM responde em um dos formatos:

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

ParametroTipoDescricao
aRuleStringRegra de validacao em linguagem natural
aErrorMessageStringMensagem de erro customizada (opcional). Se vazio, usa o motivo retornado pelo LLM
aRunAtTSkillRunAtMomento 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:

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

ParametroTipoDescricao
aSourceFieldStringNome do campo [Campo] com o texto a analisar
aTargetFieldStringNome do campo [Campo] que recebera o sentimento
aRunAtTSkillRunAtMomento de execucao (padrao: srBeforeInsert)

Valores Possiveis

ValorDescricao
POSITIVOTexto com sentimento positivo
NEGATIVOTexto com sentimento negativo
NEUTROTexto 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.