je-query-builder-custom-type
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
Install
npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/jet-engine/je-query-builder-custom-type
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart
git clone https://github.com/Lonsdale201/wp-agent-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lonsdale201/wp-agent-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
JetEngine Query Builder custom type
Build a saved-query type as two coordinated components: a runtime query and an admin editor. Keep the runtime authoritative; editor controls, REST inputs, filters, and MCP-created settings are all untrusted input to that runtime.
When to use this skill
- Expose a custom table, service, or repository to Query Builder/Listings.
- Add query-type-specific editor controls.
- Diagnose abstract-class fatals after a JetEngine upgrade.
- Fix dynamic arguments, filters, cache, count, or pagination behavior.
- Make a custom type intentionally usable from saved-query REST or MCP tooling.
Architecture and registration
Use the same vendor-prefixed slug in both registrations.
add_action(
'jet-engine/query-builder/queries/register',
static function($factory): void {
require_once __DIR__ . '/src/class-my-plugin-query.php';
$factory::register_query('my-plugin-records', My_Plugin_Query::class);
}
);
add_action(
'jet-engine/query-builder/query-editor/register',
static function($editor): void {
require_once __DIR__ . '/src/class-my-plugin-query-editor.php';
$editor->register_type(new My_Plugin_Query_Editor());
}
);
The runtime base has six required methods in 3.8.14:
_get_items()
get_items_total_count()
get_items_page_count()
get_items_pages_count()
get_current_items_page()
set_filtered_prop($prop = '', $value = null)
Omitting set_filtered_prop() leaves the subclass abstract and causes a fatal
when JetEngine instantiates it.
Runtime skeleton
use Jet_Engine\Query_Builder\Queries\Base_Query;
final class My_Plugin_Query extends Base_Query {
private function args(): array {
$this->setup_query();
$args = $this->get_query_args();
return array(
'status' => sanitize_key($args['status'] ?? 'active'),
'page' => max(1, absint($args['page'] ?? 1)),
'per_page' => min(100, max(1, absint($args['per_page'] ?? 20))),
);
}
public function _get_items() {
return my_plugin_repository()->find($this->args());
}
public function get_items_total_count() {
$cached = $this->get_cached_data('count');
if (false !== $cached) {
return (int) $cached;
}
$count = (int) my_plugin_repository()->count($this->args());
$this->update_query_cache($count, 'count');
return $count;
}
public function get_items_per_page() {
return $this->args()['per_page'];
}
public function get_current_items_page() {
return $this->args()['page'];
}
public function get_items_pages_count() {
return max(1, (int) ceil(
$this->get_items_total_count() / $this->get_items_per_page()
));
}
public function get_items_page_count() {
return count($this->get_items());
}
public function set_filtered_prop($prop = '', $value = null) {
if ('_page' === $prop) {
$this->final_query['page'] = max(1, absint($value));
return;
}
$this->merge_default_props($prop, $value);
}
}
Base_Query::get_items() automatically reads and writes the item cache. Do not
duplicate item caching in _get_items(). Counts and auxiliary requests need
their own keys. Always test cache lookup with false !== $cached, because zero
and an empty array are valid cached values.
Query setup and filtering
Call setup_query() or get_query_args() before reading final arguments. It:
- merges saved and dynamic values;
- resolves JetEngine macros;
- merges
_id-addressed nested groups; - explodes properties declared by
get_args_to_explode(); - adds
_query_typeandqueried_object_id.
Do not read $this->query as the executable query, and do not call
merge_dynamic_nested_args() on the entire final query. That helper accepts a
single nested group with an args member; setup_query() invokes it correctly.
In set_filtered_prop(), validate each property and preserve restrictions.
Intersect allowlists/IDs when a filter must narrow the base query. Blindly
replacing a tenant, owner, status, or visibility restriction can expose data.
Pagination and cache invariants
- Apply the same normalized filters to item and count queries.
- Include page/offset, site, locale, user/tenant, permissions, and all dynamic inputs in the effective cache hash when they affect results.
- Set a finite
cache_expiresfor external or frequently changing data. - Invalidate domain caches after writes; JetEngine cannot infer external data changes.
- Return objects with stable IDs and fields that Listings can consume.
- Implement
reset_query()/query_was_changed()if the class holds an inner query object or mutable state beyondfinal_query.
Editor, REST, and MCP boundaries
The editor subclass must at least implement get_id() and get_name(). Return
a component name/template/file only if custom controls are required. JetEngine
enqueues editor component files with an empty dependency array; ensure required
globals are already provided by the Query Builder page, or enqueue a separate
dependency-aware bundle.
Saved Query Builder queries can opt into a REST endpoint. The type must still sanitize every runtime argument and preserve access constraints; endpoint permission comes from the saved query's access settings, not the custom class.
JetEngine's MCP add-query tool lists registered custom type slugs, but it does
not understand their arguments automatically. Without a converter,
converted_args is empty and the saved type-specific settings may be empty.
For intentional MCP support:
- provide static
mcp_description()for schema guidance; and - return a converter through
jet-engine/query-builder/mcp/get-converter/my-plugin-records.
This MCP feature creates saved queries; it does not turn every saved query into an independently callable MCP tool.
Read runtime-editor-mcp.md when building the editor UI, filter semantics, REST exposure, or MCP conversion.
Verification
Test empty results, cached empty results, zero count, two pages, final partial page, out-of-range page, filter narrowing, attempted restriction widening, macro changes, two identical cached calls, mutation invalidation, editor save / reload, and REST permissions if enabled. For MCP support, create a query and inspect the persisted type-specific settings rather than only the returned ID.
References
- Official documentation: https://crocoblock.com/knowledge-base/plugins/jetengine/
- Crocoblock developer documentation: https://github.com/Crocoblock/developer-documentation/tree/main/01-jet-engine
- Verified source paths:
wp-content/plugins/jet-engine/includes/components/query-builder/queries/base.phpwp-content/plugins/jet-engine/includes/components/query-builder/query-factory.phpwp-content/plugins/jet-engine/includes/components/query-builder/query-editor.phpwp-content/plugins/jet-engine/includes/components/query-builder/editor/base.phpwp-content/plugins/jet-engine/includes/components/query-builder/rest-api/query-endpoint.phpwp-content/plugins/jet-engine/includes/components/query-builder/mcp/controller.phpwp-content/plugins/jet-engine/includes/components/query-builder/mcp/tool-add-query.php
Files (wp-agent-skills)
-
agents
-
openai.yaml 253 B
interface: display_name: "JetEngine Query Builder Type" short_description: "Build custom JetEngine query types" default_prompt: "Use $je-query-builder-custom-type to implement or audit this custom JetEngine Query Builder runtime and editor type."
-
-
references
-
runtime-editor-mcp.md 4.6 KB
# Query Builder runtime, editor, REST, and MCP reference Load this reference when implementing the editor component, nested controls, filter merging, REST exposure, or MCP query creation. ## Editor base contract ```php use Jet_Engine\Query_Builder\Query_Editor\Base_Query; final class My_Plugin_Query_Editor extends Base_Query { public function get_id() { return 'my-plugin-records'; } public function get_name() { return __('My Plugin records', 'my-plugin'); } public function editor_component_name() { return 'my-plugin-records-query'; } public function editor_component_file() { return plugins_url('assets/query-editor.js', MY_PLUGIN_FILE); } public function editor_component_template() { ob_start(); require __DIR__ . '/../templates/query-editor.php'; return ob_get_clean(); } public function editor_component_data() { return array('statuses' => my_plugin_allowed_statuses()); } } ``` Keep `get_id()` identical to the runtime slug. Escape translated labels for the actual Vue/HTML context. Do not interpolate untrusted values into a template or inline script. JetEngine localizes component data as an object named from the handle `jet-query-component-{type}`, with hyphens converted to underscores. ## Dynamic and nested settings Give repeatable/nested rows stable `_id` values. JetEngine saves dynamic overrides by row ID and `setup_query()` merges them. Renumbering or regenerating IDs on each editor load disconnects saved dynamic values. For a nested group, the shape is conceptually: ```php array( 'is_group' => true, '_id' => 'stable-group-id', 'relation' => 'AND', 'args' => array( array( '_id' => 'stable-row-id', 'field' => 'status', 'operator'=> '=', 'value' => 'active', ), ), ) ``` The runtime must allowlist fields, operators, relation values, order fields, and cast values. Editor restrictions are usability, not server validation. ## Filter merging `set_filtered_prop()` is a security boundary when a frontend filter changes a saved query. Use one of these policies per property: - replace: safe presentation property such as page; - merge: independent additive constraints; - intersect: IDs/statuses/tenants where filters may only narrow; - reject: internal credentials, table names, raw SQL, ownership scope. If an intersection becomes empty, use an explicit no-results sentinel rather than removing the restriction. ## REST saved-query endpoint `Base_Query::maybe_register_rest_api_endpoint()` registers only when the saved query enables it and provides a namespace and path. `Query_Endpoint` applies the saved access mode/capability/roles. Audit: - public versus role/capability settings; - schema arguments and runtime sanitization; - object-level visibility of returned records; - pagination bounds and denial-of-service costs; - cache variation for authenticated users; - output fields and sensitive repository columns. ## MCP conversion The MCP add-query tool obtains a converter through: ```php add_filter( 'jet-engine/query-builder/mcp/get-converter/my-plugin-records', static function($converter) { if ($converter) { return $converter; } return new My_Plugin_Query_MCP_Converter(); } ); ``` Match the converter interface used in `includes/components/query-builder/mcp/converters/converter-interface.php`. Allowlist input keys, cast values, cap page sizes, reject raw SQL/identifiers, and return the exact settings shape the editor/runtime expects. Without this filter, the custom slug can appear in the MCP schema while the tool persists an empty custom argument array. Treat that as unsupported, not automatic compatibility. ## Personalized cache hashes If results depend on state not already present in `final_query`, extend the hash explicitly: ```php public function get_query_hash_args() { $args = parent::get_query_hash_args(); $args['my_plugin_user_id'] = get_current_user_id(); $args['my_plugin_blog_id'] = get_current_blog_id(); return $args; } ``` Add only deterministic scalar/array dimensions. Do not put credentials or non-serializable service objects into the hash. ## Regression checklist ```text runtime class is concrete runtime/editor IDs match saved -> reload preserves every setting dynamic nested values merge by stable _id item cache distinguishes effective queries count cache accepts 0 filters cannot widen protected scope empty intersection yields no results REST public access is intentional MCP-created settings equal editor-created settings ```
-
-
SKILL.md 8.1 KB
--- name: je-query-builder-custom-type description: >- 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, REST endpoint exposure, editor assets, and optional MCP argument conversion. Use for custom tables, HPOS-like repositories, external APIs, or bugs involving stale cache, broken filters, missing editor controls, empty MCP-created queries, or pagination. metadata: wp-skills-author: "Soczó Kristóf" wp-skills-contact: "mailto:lonsdale201@hotmail.com" wp-skills-plugin: "jet-engine" wp-skills-plugin-version-tested: "3.8.14" wp-skills-wp-version-tested: "7.0.4" wp-skills-php-min: "7.4" wp-skills-last-updated: "2026-08-17" --- # JetEngine Query Builder custom type Build a saved-query type as two coordinated components: a runtime query and an admin editor. Keep the runtime authoritative; editor controls, REST inputs, filters, and MCP-created settings are all untrusted input to that runtime. ## When to use this skill - Expose a custom table, service, or repository to Query Builder/Listings. - Add query-type-specific editor controls. - Diagnose abstract-class fatals after a JetEngine upgrade. - Fix dynamic arguments, filters, cache, count, or pagination behavior. - Make a custom type intentionally usable from saved-query REST or MCP tooling. ## Architecture and registration Use the same vendor-prefixed slug in both registrations. ```php add_action( 'jet-engine/query-builder/queries/register', static function($factory): void { require_once __DIR__ . '/src/class-my-plugin-query.php'; $factory::register_query('my-plugin-records', My_Plugin_Query::class); } ); add_action( 'jet-engine/query-builder/query-editor/register', static function($editor): void { require_once __DIR__ . '/src/class-my-plugin-query-editor.php'; $editor->register_type(new My_Plugin_Query_Editor()); } ); ``` The runtime base has six required methods in 3.8.14: ```text _get_items() get_items_total_count() get_items_page_count() get_items_pages_count() get_current_items_page() set_filtered_prop($prop = '', $value = null) ``` Omitting `set_filtered_prop()` leaves the subclass abstract and causes a fatal when JetEngine instantiates it. ## Runtime skeleton ```php use Jet_Engine\Query_Builder\Queries\Base_Query; final class My_Plugin_Query extends Base_Query { private function args(): array { $this->setup_query(); $args = $this->get_query_args(); return array( 'status' => sanitize_key($args['status'] ?? 'active'), 'page' => max(1, absint($args['page'] ?? 1)), 'per_page' => min(100, max(1, absint($args['per_page'] ?? 20))), ); } public function _get_items() { return my_plugin_repository()->find($this->args()); } public function get_items_total_count() { $cached = $this->get_cached_data('count'); if (false !== $cached) { return (int) $cached; } $count = (int) my_plugin_repository()->count($this->args()); $this->update_query_cache($count, 'count'); return $count; } public function get_items_per_page() { return $this->args()['per_page']; } public function get_current_items_page() { return $this->args()['page']; } public function get_items_pages_count() { return max(1, (int) ceil( $this->get_items_total_count() / $this->get_items_per_page() )); } public function get_items_page_count() { return count($this->get_items()); } public function set_filtered_prop($prop = '', $value = null) { if ('_page' === $prop) { $this->final_query['page'] = max(1, absint($value)); return; } $this->merge_default_props($prop, $value); } } ``` `Base_Query::get_items()` automatically reads and writes the item cache. Do not duplicate item caching in `_get_items()`. Counts and auxiliary requests need their own keys. Always test cache lookup with `false !== $cached`, because zero and an empty array are valid cached values. ## Query setup and filtering Call `setup_query()` or `get_query_args()` before reading final arguments. It: - merges saved and dynamic values; - resolves JetEngine macros; - merges `_id`-addressed nested groups; - explodes properties declared by `get_args_to_explode()`; - adds `_query_type` and `queried_object_id`. Do not read `$this->query` as the executable query, and do not call `merge_dynamic_nested_args()` on the entire final query. That helper accepts a single nested group with an `args` member; `setup_query()` invokes it correctly. In `set_filtered_prop()`, validate each property and preserve restrictions. Intersect allowlists/IDs when a filter must narrow the base query. Blindly replacing a tenant, owner, status, or visibility restriction can expose data. ## Pagination and cache invariants - Apply the same normalized filters to item and count queries. - Include page/offset, site, locale, user/tenant, permissions, and all dynamic inputs in the effective cache hash when they affect results. - Set a finite `cache_expires` for external or frequently changing data. - Invalidate domain caches after writes; JetEngine cannot infer external data changes. - Return objects with stable IDs and fields that Listings can consume. - Implement `reset_query()`/`query_was_changed()` if the class holds an inner query object or mutable state beyond `final_query`. ## Editor, REST, and MCP boundaries The editor subclass must at least implement `get_id()` and `get_name()`. Return a component name/template/file only if custom controls are required. JetEngine enqueues editor component files with an empty dependency array; ensure required globals are already provided by the Query Builder page, or enqueue a separate dependency-aware bundle. Saved Query Builder queries can opt into a REST endpoint. The type must still sanitize every runtime argument and preserve access constraints; endpoint permission comes from the saved query's access settings, not the custom class. JetEngine's MCP add-query tool lists registered custom type slugs, but it does not understand their arguments automatically. Without a converter, `converted_args` is empty and the saved type-specific settings may be empty. For intentional MCP support: 1. provide static `mcp_description()` for schema guidance; and 2. return a converter through `jet-engine/query-builder/mcp/get-converter/my-plugin-records`. This MCP feature creates saved queries; it does not turn every saved query into an independently callable MCP tool. Read [runtime-editor-mcp.md](references/runtime-editor-mcp.md) when building the editor UI, filter semantics, REST exposure, or MCP conversion. ## Verification Test empty results, cached empty results, zero count, two pages, final partial page, out-of-range page, filter narrowing, attempted restriction widening, macro changes, two identical cached calls, mutation invalidation, editor save / reload, and REST permissions if enabled. For MCP support, create a query and inspect the persisted type-specific settings rather than only the returned ID. ## References - Official documentation: <https://crocoblock.com/knowledge-base/plugins/jetengine/> - Crocoblock developer documentation: <https://github.com/Crocoblock/developer-documentation/tree/main/01-jet-engine> - Verified source paths: - `wp-content/plugins/jet-engine/includes/components/query-builder/queries/base.php` - `wp-content/plugins/jet-engine/includes/components/query-builder/query-factory.php` - `wp-content/plugins/jet-engine/includes/components/query-builder/query-editor.php` - `wp-content/plugins/jet-engine/includes/components/query-builder/editor/base.php` - `wp-content/plugins/jet-engine/includes/components/query-builder/rest-api/query-endpoint.php` - `wp-content/plugins/jet-engine/includes/components/query-builder/mcp/controller.php` - `wp-content/plugins/jet-engine/includes/components/query-builder/mcp/tool-add-query.php`
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.