# JetEngine — Automação Headless e Integração com Agentes IA

Arquitectura técnica para manipular o JetEngine sem wp-admin: via PHP interno, REST API, SQL e n8n.

---

## 1. Estrutura de Dados — Base de Dados

### A. Custom Post Types (CPT) — Estrutura Standard WP

```sql
-- Dados em wp_posts
SELECT * FROM wp_posts WHERE post_type = 'meu_cpt' AND post_status = 'publish';

-- Meta fields em wp_postmeta
SELECT meta_key, meta_value FROM wp_postmeta WHERE post_id = 123;
```

**Limitação:** Queries complexas com múltiplos meta fields são lentas (JOIN por cada meta key).

### B. Custom Content Types (CCT) — Tabelas SQL Próprias

Cada CCT tem tabela dedicada: `wp_jet_cct_{slug_do_cct}`

```sql
-- Exemplo: CCT "faturas" → tabela wp_jet_cct_faturas
-- Colunas são os campos definidos no JetEngine

-- Ler todos os itens activos
SELECT * FROM wp_jet_cct_faturas WHERE cct_status = 'active';

-- Filtrar por campo
SELECT * FROM wp_jet_cct_clientes
WHERE cidade = 'Lisboa'
AND cct_status = 'publish'
ORDER BY _created_at DESC
LIMIT 20;

-- Campos especiais em todos os CCT:
-- _ID              → ID numérico único
-- cct_status       → publish / draft
-- cct_author_id    → ID do utilizador criador
-- _created_at      → Unix timestamp de criação
-- _modified        → Unix timestamp de modificação
```

**Vantagem:** SQL directo é seguro e extremamente rápido. Ideal para leitura em dashboards e n8n.

### C. Relations — Tabelas wp_jet_rel_default e _meta

O JetEngine NÃO guarda relações em `wp_postmeta`. Tem tabela própria.

```sql
-- wp_jet_rel_default: relações básicas
-- parent_object_id | child_object_id | rel_id (hash/slug da relação)

-- Ler filhos de um pai
SELECT child_object_id
FROM wp_jet_rel_default
WHERE parent_object_id = 123
AND rel_id = 'relacao_clientes_projetos';

-- Ler pai a partir de filho
SELECT parent_object_id
FROM wp_jet_rel_default
WHERE child_object_id = 456
AND rel_id = 'relacao_clientes_projetos';

-- Contar relações
SELECT parent_object_id, COUNT(*) as total
FROM wp_jet_rel_default
WHERE rel_id = 'relacao_empresa_colaboradores'
GROUP BY parent_object_id;
```

**wp_jet_rel_default_meta** — metadados específicos de uma relação (ex: papel do actor num filme):

```sql
-- Exemplo: condição de uso de um veículo numa relação utilizador↔veículo
SELECT meta_key, meta_value
FROM wp_jet_rel_default_meta
WHERE rel_id = 8
AND parent_object_id = 45
AND child_object_id = 1020;

-- UPSERT massivo para sincronizações ETL (com integridade referencial)
START TRANSACTION;
INSERT INTO wp_jet_rel_default (rel_id, parent_object_id, child_object_id)
VALUES (8, 45, 1020), (8, 45, 1021), (8, 92, 1055)
ON DUPLICATE KEY UPDATE parent_object_id = VALUES(parent_object_id);

INSERT INTO wp_jet_rel_default_meta (rel_id, parent_object_id, child_object_id, meta_key, meta_value)
VALUES (8, 45, 1020, 'condicao_uso', 'Alugado')
ON DUPLICATE KEY UPDATE meta_value = VALUES(meta_value);
COMMIT;
-- OBRIGATÓRIO após SQL directo: chamar wp_cache_flush() + jet_smart_filters()->indexer->index_all()
```

**Nota:** Em relações críticas com muito tráfego, o JetEngine pode isolar por relação em tabela própria (`wp_jet_rel_81_meta`), reduzindo table locks.

**Guardrail:** NUNCA inserir directamente via SQL para criar relações em ambientes activos. Usar API PHP (ver secção 2). SQL directo só para sincronizações ETL massivas (centenas de milhares de linhas).

### D. Custom Meta Storage para CPT (Tabela Dedicada)

Alternativa ao `wp_postmeta` para CPTs com muitos campos e consultas complexas.

```
JetEngine > Post Types > Edit > Custom Meta Storage: Activar
→ Cria tabela dedicada: wp_{cpt_slug}_meta
  (ex: wp_imoveis_meta com colunas tipadas por campo)
```

```sql
-- Com Custom Meta Storage activo, cada campo é uma coluna
SELECT preco, area_m2, tipologia, localizacao
FROM wp_imoveis_meta
WHERE preco BETWEEN 150000 AND 350000
AND tipologia IN ('T2', 'T3')
ORDER BY preco ASC;

-- Joins com wp_posts permanecem necessários para title/date
SELECT p.ID, p.post_title, m.preco, m.area_m2
FROM wp_posts p
JOIN wp_imoveis_meta m ON m.post_id = p.ID
WHERE p.post_status = 'publish'
AND m.preco < 300000;
```

**Quando usar:** CPTs com >10 campos numéricos/date, queries com múltiplos filtros simultâneos, alternativa a CCT quando se precisa de taxonomias e comentários nativos WP.

### E. Estrutura na wp_options

```sql
-- Configurações de CPTs
SELECT option_value FROM wp_options WHERE option_name = 'jet_engine_cpt';

-- Taxonomias
SELECT option_value FROM wp_options WHERE option_name = 'jet_engine_tax';

-- Relações
SELECT option_value FROM wp_options WHERE option_name = 'jet_engine_relations';

-- Meta Boxes
SELECT option_value FROM wp_options WHERE option_name = 'jet_engine_meta_boxes';
```

**Aviso:** São PHP serialized arrays. NUNCA editar via SQL string replace — vai corromper. Usar sempre `unserialize → edit → serialize` em PHP.

---

## 2. PHP — API Interna JetEngine

### Criar/Actualizar Item CCT

```php
<?php
// Usar CCT Data Handler (mantém hooks, validações e cache)
// Mais seguro que SQL directo para escrita

$cct_slug = 'faturas';
$item_data = [
    'titulo'  => 'Fatura #1024',
    'valor'   => 500.00,
    'status'  => 'pendente',
    'cliente' => 'Empresa X Lda',
    // Para update, incluir _ID existente:
    // '_ID' => 15
];

// Aceder ao módulo CCT
if (function_exists('jet_engine')) {
    $cct_module = jet_engine()->modules->get_module('custom-content-types');
    if ($cct_module) {
        $manager = $cct_module->instance->manager;
        $type    = $manager->get_content_type_instance($cct_slug);
        if ($type) {
            $result = $type->get_handler_instance()->update_item($item_data);
            // $result = ID do item criado, ou false em caso de erro
        }
    }
}
```

### Gerir Relações Programaticamente

```php
<?php
// Método 1: process_relation (simples, para connect/disconnect)
// IMPORTANTE: Executar após o hook 'init' com prioridade > 12

if (class_exists('\Jet_Engine\Relations\Manager')) {
    $relations_manager = \Jet_Engine\Relations\Manager::get_instance();
    $relations_manager->process_relation(
        $rel_id,   // ex: 'relacao_empresa_colaboradores'
        $parent_id,
        $child_id,
        true       // true = conectar, false = desconectar
    );
}

// Método 2: API completa (update com contexto — mais robusto)
function ia_conectar_com_contexto($cliente_id, $documento_id) {
    $rel_id_numerico = 15; // ID numérico da relação (ver JetEngine > Relations)

    $relation = jet_engine()->relations->get_active_relations($rel_id_numerico);

    if ($relation) {
        $relation->set_update_context('parent'); // perspectiva: 'parent' ou 'child'
        $relation->update($cliente_id, $documento_id);

        // Limpar cache do Query Builder após actualizar relações
        \Jet_Engine\Query_Builder\Manager::instance()->listings_cache->clear();
    }
}
```

### Criar/Actualizar CPT Post

```php
<?php
// CPTs usam funções standard do WordPress
$post_data = [
    'post_title'   => 'Novo Projecto',
    'post_content' => '',
    'post_status'  => 'publish',
    'post_type'    => 'projecto',
];

$post_id = wp_insert_post($post_data);

if (!is_wp_error($post_id)) {
    update_post_meta($post_id, 'cliente_id', 45);
    update_post_meta($post_id, 'valor_contrato', 5000);
    update_post_meta($post_id, 'data_inicio', '2026-03-01');
}
```

### Manipular Estrutura (Onboarding Programático)

```php
<?php
// Adicionar novo CPT à configuração do JetEngine
// CUIDADO: substitui configurações existentes se não fizer merge

$current_cpts = get_option('jet_engine_cpt', []);

$new_cpt = [
    'slug'          => 'imovel',
    'singular_name' => 'Imóvel',
    'name'          => 'Imóveis',
    'show_in_rest'  => true,
    'supports'      => ['title', 'editor', 'thumbnail', 'custom-fields'],
    // ... outras configurações
];

$current_cpts[] = $new_cpt;
update_option('jet_engine_cpt', $current_cpts);

// Obrigatório: regenerar permalinks após alterar CPTs
flush_rewrite_rules();
```

### Registar Meta Box Programaticamente (Hook)

```php
<?php
// Hook: jet-engine/meta-boxes/register-instances
// Ideal para plugins de agência (controlo por código, imutável pelo cliente)

add_action('jet-engine/meta-boxes/register-instances', function($meta_manager) {

    $meta_fields = [
        [
            'title'       => 'ID Remoto de Sincronização',
            'name'        => '_ia_sync_id',
            'object_type' => 'field',
            'type'        => 'text',
            'width'       => '50%',
        ],
        [
            'title'       => 'Score de Auditoria',
            'name'        => '_score_auditoria',
            'object_type' => 'field',
            'type'        => 'number',
            'width'       => '50%',
        ],
    ];

    $post_type = 'documento_legal';

    if (!class_exists('Jet_Engine_CPT_Meta')) {
        require $meta_manager->component_path('post.php');
    }

    $meta_manager->store_fields($post_type, $meta_fields, 'post_type');
    new Jet_Engine_CPT_Meta($post_type, $meta_fields, 'Controlo IA', 'normal', 'high', [
        'object_type'       => 'post',
        'allowed_post_type' => [$post_type],
        'name'              => 'Controlo de Metadados IA',
    ]);
});
```

### WP-CLI Batch Import para CCT

```php
<?php
// Ficheiro: import_cct_data.php
// Executar: wp eval-file import_cct_data.php

if (!class_exists('\Jet_Engine\Modules\Custom_Content_Types\Module')) {
    WP_CLI::error('Módulo CCT não está activo.');
}

$cct_slug   = 'veiculos';
$manager    = \Jet_Engine\Modules\Custom_Content_Types\Module::instance()->manager;
$content_type = $manager->get_content_types($cct_slug);

if (!$content_type) {
    WP_CLI::error("CCT '{$cct_slug}' não encontrado.");
}

$registos = json_decode(file_get_contents('/tmp/veiculos.json'), true);
$sucesso  = 0;

foreach ($registos as $dados) {
    // db->insert() respeita sanitizações e integridade do CCT
    if ($content_type->db->insert($dados)) {
        $sucesso++;
    }
    // Libertar memória em loops grandes
    if ($sucesso % 500 === 0) {
        wp_cache_flush();
    }
}

wp_cache_flush();
WP_CLI::success("Importação concluída. Registos criados: {$sucesso}");
```

```bash
# Execução no servidor CWP:
PHP="/opt/alt/php-fpm83/usr/bin/php"
WP="$PHP /usr/local/bin/wp --allow-root --path=/home/USER/public_html"
$WP eval-file /tmp/import_cct_data.php
```

---

## 3. REST API JetEngine

### Endpoints

| Tipo | Endpoint | Método |
|------|----------|--------|
| CPT | `/wp-json/wp/v2/{cpt_slug}` | GET / POST / PUT / DELETE |
| CCT | `/wp-json/jet-cct/{cct_slug}` | GET / POST / PUT / DELETE |
| Relação | `/wp-json/jet-rel/{relation_id}` | GET / POST |

**Pré-requisito:** Activar REST API nas configurações do CPT/CCT/Relação em JetEngine.

### Autenticação

```bash
# Application Passwords (WP 5.6+) — recomendado para n8n
# Criar em: WP Admin > Users > Edit > Application Passwords

# Header de autenticação
Authorization: Basic $(echo -n 'username:app-password' | base64)
```

### Criar/Ler Item CCT via REST

```bash
# Criar registo no CCT "clientes"
curl -X POST https://site.pt/wp-json/jet-cct/clientes \
  -H "Authorization: Basic dXNlcjpwYXNz..." \
  -H "Content-Type: application/json" \
  -d '{
    "nome_cliente": "Empresa X Lda",
    "email": "contacto@empresax.com",
    "nif": "123456789",
    "cct_status": "publish"
  }'
# Resposta: { "_ID": 42, ... }

# Pesquisa com filtros — parâmetros CCT REST API
# _limit, _offset: paginação
# _cct_search: pesquisa de texto
# _cct_search_by: campo específico
curl "https://site.pt/wp-json/jet-cct/clientes?_limit=20&_offset=0&_cct_search=Lisboa&_cct_search_by=cidade"
# Header de resposta inclui: Jet-Query-Total (total de registos para paginação)
```

### Actualizar Relação via REST API

```bash
# POST para /wp-json/jet-rel/{relation_id}
# store_items_type: "update" (aditivo) | "replace" (destrutivo!) | "disconnect" (remover)
curl -X POST https://site.pt/wp-json/jet-rel/22 \
  -H "Authorization: Basic dXNlcjpwYXNz..." \
  -H "Content-Type: application/json" \
  -d '{
    "parent_id": 4500,
    "child_id": 1020,
    "context": "child",
    "store_items_type": "update",
    "meta": {
      "etapa_validacao": "Aprovado",
      "score_auditoria": 88.5,
      "data_processamento": "2026-02-18T08:30"
    }
  }'
```

**Atenção `store_items_type`:**
- `update` — adiciona à relação existente (safe)
- `replace` — **DESTRÓI todas as relações existentes** e substitui (perigoso!)
- `disconnect` — remove a relação específica

### Criar Post (CPT) via REST

```bash
curl -X POST https://site.pt/wp-json/wp/v2/projecto \
  -H "Authorization: Basic dXNlcjpwYXNz..." \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Projecto Website ABC",
    "status": "publish",
    "meta": {
      "cliente_id": 45,
      "valor_contrato": 5000
    }
  }'
```

### Campos Repeater via REST

```json
{
  "galeria_servicos": [
    { "titulo": "Serviço A", "descricao": "Descrição A" },
    { "titulo": "Serviço B", "descricao": "Descrição B" }
  ]
}
```

---

## 4. Integração n8n

### Leitura (MySQL Node — mais rápido)

```javascript
// n8n MySQL Node — ler CCT directamente da BD
// Muito mais rápido que REST API para leitura em massa

// Query example:
SELECT
  _ID,
  nome_cliente,
  email,
  cidade,
  cct_status,
  _created_at
FROM wp_jet_cct_clientes
WHERE cct_status = 'publish'
ORDER BY _created_at DESC
LIMIT 100;
```

**Vantagem:** 100x mais rápido que HTTP Request ao WP REST API.

### Escrita (HTTP Request Node — via REST)

```javascript
// n8n HTTP Request Node
// Method: POST
// URL: https://site.pt/wp-json/jet-cct/faturas
// Authentication: Basic Auth
// Body (JSON):
{
  "numero": "{{ $json.fatura_numero }}",
  "cliente_id": "{{ $json.cliente_id }}",
  "valor": "{{ $json.total }}",
  "cct_status": "publish"
}
```

**Usar REST para escrita** (garante hooks JetEngine e validações).

### Webhooks via JetFormBuilder

```
JetFormBuilder pode disparar Webhook para n8n:
1. Criar formulário no JetFormBuilder
2. Post-submit Action: Webhook
3. URL: https://n8n.descomplicar.pt/webhook/jetengine
4. Formulários silenciosos: receber POST externos que passam pelas validações JetEngine
```

---

## 5. Tabela de Métodos

| Método | Complexidade | Performance | Risco | Caso de Uso |
|--------|-------------|-------------|-------|-------------|
| REST API | Baixa | Média | Baixo | Inserir dados externos via n8n |
| PHP (Classes Internas) | Média | Alta | Baixo | Plugins custom, scripts de migração |
| SQL Directo (CCT) | Alta | Muito Alta | Médio | Bulk updates, leitura analytics |
| SQL Directo (Meta) | Alta | Baixa | **Alto** | **Evitar** — corrupção de arrays |
| WP-CLI Custom | Média | Alta | Baixo | Manutenção, cron jobs |

---

## 6. Guardrails Críticos

### Cache JetEngine

```bash
# Após alterar estruturas via PHP/SQL na wp_options,
# forçar regeneração de cache:

PHP="/opt/alt/php-fpm83/usr/bin/php"
WP="$PHP /usr/local/bin/wp --allow-root --path=/home/USER/public_html"

$WP cache flush
$WP rewrite flush
$WP elementor flush-css --regenerate
```

**Cache do Query Builder** — obrigatório após alterações SQL directas:

```php
<?php
// Limpar cache de listagens após SQL bulk update
\Jet_Engine\Query_Builder\Manager::instance()->listings_cache->clear();

// Recalcular Indexer do JetSmartFilters (se filtros activos)
if (function_exists('jet_smart_filters')) {
    jet_smart_filters()->indexer->index_all();
}

// Cache do WordPress
wp_cache_flush();
```

**Regra crítica:** Alterações SQL directas em `wp_jet_cct_*` ou `wp_jet_rel_default` NÃO invalidam automaticamente o Object Cache nem os transients do Query Builder. O site apresentará dados obsoletos até ao flush.

### NUNCA Editar Campos Serializados via SQL String Replace

```sql
-- ERRADO — corrompe o array PHP serializado
UPDATE wp_options
SET option_value = REPLACE(option_value, 'Imóveis', 'Propriedades')
WHERE option_name = 'jet_engine_cpt';

-- CORRECTO — usar PHP
$cpts = get_option('jet_engine_cpt');
// Fazer alterações via PHP
update_option('jet_engine_cpt', $cpts);
```

### IDs de Relações em Migrações

```
Problema: Ao migrar dados entre ambientes, IDs podem mudar → relações quebram
Solução: Usar Slugs como referência no código; converter para ID antes de gravar
Usar: get_posts(['name' => 'slug-do-post', 'post_type' => 'meu_cpt'])
```

### Repeater Fields via SQL

```
Repeater fields são PHP serialize() ou JSON dependendo da configuração.
NUNCA fazer string replace em SQL.
Usar sempre: unserialize() → editar array → serialize() → update_post_meta()
```

---

## 7. MU-Plugin: Wrapper de Endpoints Custom

Para expor endpoints seguros ao n8n sem passar pela REST API standard:

```php
<?php
/**
 * Plugin Name: JetEngine AI Agent Endpoints
 * Description: Endpoints seguros para criação de CCTs e Relações via n8n
 * Must Use: sim — colocar em /wp-content/mu-plugins/
 */

add_action('rest_api_init', function() {
    // Endpoint para criar item CCT
    register_rest_route('jet-agent/v1', '/cct/(?P<slug>[a-z_]+)', [
        'methods'             => 'POST',
        'callback'            => 'jet_agent_create_cct_item',
        'permission_callback' => function() {
            return current_user_can('edit_posts');
        },
        'args' => [
            'slug' => ['required' => true, 'sanitize_callback' => 'sanitize_key'],
        ]
    ]);

    // Endpoint para gerir relações
    register_rest_route('jet-agent/v1', '/relation', [
        'methods'             => 'POST',
        'callback'            => 'jet_agent_manage_relation',
        'permission_callback' => function() {
            return current_user_can('edit_posts');
        }
    ]);
});

function jet_agent_create_cct_item(WP_REST_Request $request) {
    $slug = $request->get_param('slug');
    $data = $request->get_json_params();

    // Sanitizar dados
    $data = array_map('sanitize_text_field', $data);

    // Usar handler interno do JetEngine
    if (!function_exists('jet_engine')) {
        return new WP_Error('jet_engine_missing', 'JetEngine não activo', ['status' => 500]);
    }

    $cct_module = jet_engine()->modules->get_module('custom-content-types');
    $type       = $cct_module->instance->manager->get_content_type_instance($slug);

    if (!$type) {
        return new WP_Error('cct_not_found', "CCT '{$slug}' não encontrado", ['status' => 404]);
    }

    $result = $type->get_handler_instance()->update_item($data);

    return rest_ensure_response([
        'success' => (bool)$result,
        'id'      => $result,
    ]);
}

function jet_agent_manage_relation(WP_REST_Request $request) {
    $params    = $request->get_json_params();
    $rel_id    = sanitize_text_field($params['rel_id'] ?? '');
    $parent_id = absint($params['parent_id'] ?? 0);
    $child_id  = absint($params['child_id'] ?? 0);
    $connect   = (bool)($params['connect'] ?? true);

    if (!$rel_id || !$parent_id || !$child_id) {
        return new WP_Error('missing_params', 'rel_id, parent_id e child_id são obrigatórios', ['status' => 400]);
    }

    if (class_exists('\Jet_Engine\Relations\Manager')) {
        \Jet_Engine\Relations\Manager::get_instance()->process_relation(
            $rel_id, $parent_id, $child_id, $connect
        );
        return rest_ensure_response(['success' => true]);
    }

    return new WP_Error('relations_missing', 'Módulo Relations não disponível', ['status' => 500]);
}
```

---

*JetEngine Automation | Descomplicar® | 18-02-2026*
