{"slug":"je-query-builder-custom-type","title":"je-query-builder-custom-type","summary":"Registers or audits a custom JetEngine Query Builder type with paired runtime and editor classes. Covers all six Base_Query abstract methods including set_filtered_prop, setup_query dynamic/macro merging, pagination, automatic item caching and explicit count caching, filtering, R","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-09-16T14:51:53.01364Z","repo":{"url":"https://github.com/Lonsdale201/wp-agent-skills","stars":22,"forks":2,"license":"MIT","updatedAt":"2026-09-21T19:53:59Z"},"bodyHtml":"<hr>\n<h2>name: je-query-builder-custom-type\ndescription: &gt;-\nRegisters or audits a custom JetEngine Query Builder type with paired runtime\nand editor classes. Covers all six Base_Query abstract methods including\nset_filtered_prop, setup_query dynamic/macro merging, pagination, automatic\nitem caching and explicit count caching, filtering, REST endpoint exposure,\neditor assets, and optional MCP argument conversion. Use for custom tables,\nHPOS-like repositories, external APIs, or bugs involving stale cache, broken\nfilters, missing editor controls, empty MCP-created queries, or pagination.\nmetadata:\nwp-skills-author: \"Soczó Kristóf\"\nwp-skills-contact: \"mailto:lonsdale201@hotmail.com\"\nwp-skills-plugin: \"jet-engine\"\nwp-skills-plugin-version-tested: \"3.8.14\"\nwp-skills-wp-version-tested: \"7.0.4\"\nwp-skills-php-min: \"7.4\"\nwp-skills-last-updated: \"2026-08-17\"</h2>\n<h1>JetEngine Query Builder custom type</h1>\n<p>Build a saved-query type as two coordinated components: a runtime query and an\nadmin editor. Keep the runtime authoritative; editor controls, REST inputs,\nfilters, and MCP-created settings are all untrusted input to that runtime.</p>\n<h2>When to use this skill</h2>\n<ul>\n<li>Expose a custom table, service, or repository to Query Builder/Listings.</li>\n<li>Add query-type-specific editor controls.</li>\n<li>Diagnose abstract-class fatals after a JetEngine upgrade.</li>\n<li>Fix dynamic arguments, filters, cache, count, or pagination behavior.</li>\n<li>Make a custom type intentionally usable from saved-query REST or MCP tooling.</li>\n</ul>\n<h2>Architecture and registration</h2>\n<p>Use the same vendor-prefixed slug in both registrations.</p>\n<pre><code>add_action(\n    'jet-engine/query-builder/queries/register',\n    static function($factory): void {\n        require_once __DIR__ . '/src/class-my-plugin-query.php';\n        $factory::register_query('my-plugin-records', My_Plugin_Query::class);\n    }\n);\n\nadd_action(\n    'jet-engine/query-builder/query-editor/register',\n    static function($editor): void {\n        require_once __DIR__ . '/src/class-my-plugin-query-editor.php';\n        $editor-&gt;register_type(new My_Plugin_Query_Editor());\n    }\n);\n</code></pre>\n<p>The runtime base has six required methods in 3.8.14:</p>\n<pre><code>_get_items()\nget_items_total_count()\nget_items_page_count()\nget_items_pages_count()\nget_current_items_page()\nset_filtered_prop($prop = '', $value = null)\n</code></pre>\n<p>Omitting <code>set_filtered_prop()</code> leaves the subclass abstract and causes a fatal\nwhen JetEngine instantiates it.</p>\n<h2>Runtime skeleton</h2>\n<pre><code>use Jet_Engine\\Query_Builder\\Queries\\Base_Query;\n\nfinal class My_Plugin_Query extends Base_Query {\n    private function args(): array {\n        $this-&gt;setup_query();\n        $args = $this-&gt;get_query_args();\n\n        return array(\n            'status'   =&gt; sanitize_key($args['status'] ?? 'active'),\n            'page'     =&gt; max(1, absint($args['page'] ?? 1)),\n            'per_page' =&gt; min(100, max(1, absint($args['per_page'] ?? 20))),\n        );\n    }\n\n    public function _get_items() {\n        return my_plugin_repository()-&gt;find($this-&gt;args());\n    }\n\n    public function get_items_total_count() {\n        $cached = $this-&gt;get_cached_data('count');\n        if (false !== $cached) {\n            return (int) $cached;\n        }\n\n        $count = (int) my_plugin_repository()-&gt;count($this-&gt;args());\n        $this-&gt;update_query_cache($count, 'count');\n        return $count;\n    }\n\n    public function get_items_per_page() {\n        return $this-&gt;args()['per_page'];\n    }\n\n    public function get_current_items_page() {\n        return $this-&gt;args()['page'];\n    }\n\n    public function get_items_pages_count() {\n        return max(1, (int) ceil(\n            $this-&gt;get_items_total_count() / $this-&gt;get_items_per_page()\n        ));\n    }\n\n    public function get_items_page_count() {\n        return count($this-&gt;get_items());\n    }\n\n    public function set_filtered_prop($prop = '', $value = null) {\n        if ('_page' === $prop) {\n            $this-&gt;final_query['page'] = max(1, absint($value));\n            return;\n        }\n\n        $this-&gt;merge_default_props($prop, $value);\n    }\n}\n</code></pre>\n<p><code>Base_Query::get_items()</code> automatically reads and writes the item cache. Do not\nduplicate item caching in <code>_get_items()</code>. Counts and auxiliary requests need\ntheir own keys. Always test cache lookup with <code>false !== $cached</code>, because zero\nand an empty array are valid cached values.</p>\n<h2>Query setup and filtering</h2>\n<p>Call <code>setup_query()</code> or <code>get_query_args()</code> before reading final arguments. It:</p>\n<ul>\n<li>merges saved and dynamic values;</li>\n<li>resolves JetEngine macros;</li>\n<li>merges <code>_id</code>-addressed nested groups;</li>\n<li>explodes properties declared by <code>get_args_to_explode()</code>;</li>\n<li>adds <code>_query_type</code> and <code>queried_object_id</code>.</li>\n</ul>\n<p>Do not read <code>$this-&gt;query</code> as the executable query, and do not call\n<code>merge_dynamic_nested_args()</code> on the entire final query. That helper accepts a\nsingle nested group with an <code>args</code> member; <code>setup_query()</code> invokes it correctly.</p>\n<p>In <code>set_filtered_prop()</code>, validate each property and preserve restrictions.\nIntersect allowlists/IDs when a filter must narrow the base query. Blindly\nreplacing a tenant, owner, status, or visibility restriction can expose data.</p>\n<h2>Pagination and cache invariants</h2>\n<ul>\n<li>Apply the same normalized filters to item and count queries.</li>\n<li>Include page/offset, site, locale, user/tenant, permissions, and all dynamic\ninputs in the effective cache hash when they affect results.</li>\n<li>Set a finite <code>cache_expires</code> for external or frequently changing data.</li>\n<li>Invalidate domain caches after writes; JetEngine cannot infer external data\nchanges.</li>\n<li>Return objects with stable IDs and fields that Listings can consume.</li>\n<li>Implement <code>reset_query()</code>/<code>query_was_changed()</code> if the class holds an inner\nquery object or mutable state beyond <code>final_query</code>.</li>\n</ul>\n<h2>Editor, REST, and MCP boundaries</h2>\n<p>The editor subclass must at least implement <code>get_id()</code> and <code>get_name()</code>. Return\na component name/template/file only if custom controls are required. JetEngine\nenqueues editor component files with an empty dependency array; ensure required\nglobals are already provided by the Query Builder page, or enqueue a separate\ndependency-aware bundle.</p>\n<p>Saved Query Builder queries can opt into a REST endpoint. The type must still\nsanitize every runtime argument and preserve access constraints; endpoint\npermission comes from the saved query's access settings, not the custom class.</p>\n<p>JetEngine's MCP add-query tool lists registered custom type slugs, but it does\nnot understand their arguments automatically. Without a converter,\n<code>converted_args</code> is empty and the saved type-specific settings may be empty.\nFor intentional MCP support:</p>\n<ol>\n<li>provide static <code>mcp_description()</code> for schema guidance; and</li>\n<li>return a converter through\n<code>jet-engine/query-builder/mcp/get-converter/my-plugin-records</code>.</li>\n</ol>\n<p>This MCP feature creates saved queries; it does not turn every saved query into\nan independently callable MCP tool.</p>\n<p>Read <a href=\"references/runtime-editor-mcp.md\">runtime-editor-mcp.md</a> when building the\neditor UI, filter semantics, REST exposure, or MCP conversion.</p>\n<h2>Verification</h2>\n<p>Test empty results, cached empty results, zero count, two pages, final partial\npage, out-of-range page, filter narrowing, attempted restriction widening,\nmacro changes, two identical cached calls, mutation invalidation, editor save /\nreload, and REST permissions if enabled. For MCP support, create a query and\ninspect the persisted type-specific settings rather than only the returned ID.</p>\n<h2>References</h2>\n<ul>\n<li>Official documentation: <a href=\"https://crocoblock.com/knowledge-base/plugins/jetengine/\">https://crocoblock.com/knowledge-base/plugins/jetengine/</a></li>\n<li>Crocoblock developer documentation: <a href=\"https://github.com/Crocoblock/developer-documentation/tree/main/01-jet-engine\">https://github.com/Crocoblock/developer-documentation/tree/main/01-jet-engine</a></li>\n<li>Verified source paths:\n<ul>\n<li><code>wp-content/plugins/jet-engine/includes/components/query-builder/queries/base.php</code></li>\n<li><code>wp-content/plugins/jet-engine/includes/components/query-builder/query-factory.php</code></li>\n<li><code>wp-content/plugins/jet-engine/includes/components/query-builder/query-editor.php</code></li>\n<li><code>wp-content/plugins/jet-engine/includes/components/query-builder/editor/base.php</code></li>\n<li><code>wp-content/plugins/jet-engine/includes/components/query-builder/rest-api/query-endpoint.php</code></li>\n<li><code>wp-content/plugins/jet-engine/includes/components/query-builder/mcp/controller.php</code></li>\n<li><code>wp-content/plugins/jet-engine/includes/components/query-builder/mcp/tool-add-query.php</code></li>\n</ul>\n</li>\n</ul>\n","files":[{"path":"agents/openai.yaml","sizeBytes":253,"isText":true},{"path":"references/runtime-editor-mcp.md","sizeBytes":4714,"isText":true},{"path":"SKILL.md","sizeBytes":8311,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-16T14:56:05.611453Z","sha256":"F534C1B623684EAB393D916FA6E5EBAA8E41531870FBF1E7F12E83D260DCEF31","sizeBytes":5868},"review":null,"source":{"repositoryUrl":"https://github.com/Lonsdale201/wp-agent-skills","path":"jet-engine/je-query-builder-custom-type","license":"MIT","commit":"8820ff3c301066297e696611e3bc4ebeb47d1851","subtreeSha":"C6993C0ED26DFD695BA08FDEC4C15FC6E908AAC5CE2EE2F8A31268D1742BE84D","lastSyncedAt":"2026-09-22T13:51:11.366991Z"},"reviewedAt":"2026-09-16T15:12:28.460248Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/jet-engine/je-query-builder-custom-type"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart"},{"target":"git","command":"git clone https://github.com/Lonsdale201/wp-agent-skills.git"}]}