# 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 '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 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 '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 '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 '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 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 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 [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*