je-data-stores
Builds or audits JetEngine Data Store integrations for favorites, bookmarks, likes, recently viewed items, user IDs, and CCT IDs. Covers cookie, session, user-meta, local-storage, and user-IP semantics; Factory lookup; frontend and programmatic mutation; Query Builder; counters;
Install
npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/jet-engine/je-data-stores
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 Data Stores
Use Data Stores for preference-like collections such as favorites, bookmarks, likes, comparisons, and recently viewed items. Storage membership and counters are user-controlled signals, not authentication, authorization, payment, license, course access, or entitlement data.
When to use this skill
- Render add/remove buttons or query a configured Data Store.
- Read or mutate a store from a companion plugin.
- Choose among cookie, session, user metadata, local storage, and user IP.
- Store user IDs or CCT item IDs instead of post IDs.
- Add a custom store type.
- Audit anonymous AJAX, max-size behavior, counts, caching, or synchronization.
Module timing and lookup
The Data Stores module initializes on WordPress init priority 0. Its configured
stores are available afterward:
add_action('init', static function(): void {
if (! class_exists('Jet_Engine\\Modules\\Data_Stores\\Module')) {
return;
}
$module = \Jet_Engine\Modules\Data_Stores\Module::instance();
$store = $module->stores
? $module->stores->get_store('my_favorites')
: false;
if (! $store) {
return;
}
$items = $store->get_store();
}, 20);
Allowlist the slug; do not expose arbitrary store selection from request data.
Factory::in_store() uses non-strict in_array(), so normalize IDs at your
boundary when type distinctions matter.
Storage-type matrix
| Type | State location | PHP-readable | Identity/reliability notes |
|---|---|---|---|
cookies |
HttpOnly browser cookie, one-year expiry | Yes, on requests carrying it | Client-owned, size-limited by cookie constraints |
session |
PHP session | Yes | Starts session during parse_request; audit full-page cache and session locking |
user-meta |
je_data_store_{slug} user meta |
Yes | Logged-in user only; values are normalized to strings |
local-storage |
browser localStorage |
No | PHP get/add/remove methods are empty; frontend/query bridge supplies IDs |
user_ip |
JetEngine custom table keyed by MD5 of detected IP | Yes | IP/header-derived and collision/shared-network prone; not identity |
is_user means the stored item IDs represent WordPress users; it does not mean
the store belongs to the current user. Choose user-meta for per-account
persistence. User stores are server-side only.
Read path
Use the configured factory:
$type = $store->get_type();
$type_id = $type->type_id();
$items = $type->is_front_store() ? null : (array) $store->get_store();
$contains = null === $items ? null : in_array($item_id, $items, true);
$size = $store->get_size();
$can_count = $store->can_count_posts();
For local storage, PHP get_store() returns null; calling the Factory
get_count() or in_store() wrappers can consequently reach count(null) or
in_array(..., null) on PHP 8. Use a Data Store Listing/Query Builder
configuration so JetEngine's frontend bridge passes the IDs. Never silently
treat a missing server result as an empty browser store.
Mutation paths are not equivalent
The frontend add/remove AJAX handlers orchestrate max size, on-view eviction,
filtered-id, counts, fragments, and lifecycle hooks. Factory has no public
high-level add()/remove() wrapper. Calling:
$store->get_type()->add_to_store($store->get_slug(), $item_id);
$store->get_type()->remove($store->get_slug(), $item_id);
changes backend state but bypasses Factory size checks, count maintenance, fragments, and add/remove hooks. JetFormBuilder's built-in add action also calls the type directly. For programmatic writes, create one integration-owned service that validates item/actor, applies the intended size policy, maps IDs, checks an actual membership transition, updates/rebuilds counts, and emits your own idempotent domain event. Mirror JetEngine's internal AJAX sequence only when compatibility with those exact hooks is required.
In 3.8.14, before-remove-from-store fires after the store type has already
removed the value, despite its name. Do not use it to veto removal or read the
pre-mutation state. Also regression-test bounded store_on_view plus item
counters: automatic oldest-item eviction does not reconcile the evicted item's
counter through the normal decrement path.
Anonymous AJAX and trust boundary
Every configured server-side store registers authenticated and nopriv add /
remove actions. The request validates post_id and matching store slug, but no
nonce, capability, or object-ownership check is performed. This supports public
favorites/likes. Consequences:
- do not use membership as proof of identity or permission;
- do not use counts as fraud-resistant analytics or billing data;
- validate whether an item is public/eligible in companion logic when required;
- rate-limit abusive public writes outside JetEngine if counts/resources matter;
- remember that a nonce would mitigate CSRF, not make a public signal trusted.
The admin clear action is different: it requires manage_options and a valid
jet-engine-data-stores nonce, resets counters, then clears supported storage.
Query Builder and CCT
Use Query Builder type data-stores-query to return stored posts or users in
store order and apply supported post/user filters. Local storage is resolved on
the frontend and load-more path. Set a finite max_items; an empty store must
produce no results, not an unrestricted query.
For a CCT store, enable is_cct and choose related_cct. JetEngine maps listing
objects to CCT _ID, delegates the query to CCT Query Builder, preserves stored
ID order, and stores optional counts in a generated {store_slug}_count CCT
service column. A store cannot be both is_user and CCT. Test the CCT/local
storage frontend bridge, filters, load more, and counter resets.
Custom store types
Register an instance extending
Jet_Engine\Modules\Data_Stores\Stores\Base_Store on
jet-engine/data-stores/register-store-types. Implement type_id, type_name,
add_to_store, remove, and get. If browser-only, implement the JS methods
and return true from is_front_store().
The manager reuses one store-type object for all configured stores and calls
its on_init() from every Factory. Make initialization idempotent and keep
per-store state keyed by slug rather than in one mutable property. Sanitize both
store slug and item shape, define concurrency behavior, and implement clearing
only if it is safe and complete.
Read storage-mutation-query.md before programmatic mutation, custom types, count-sensitive work, or CCT integration.
Verification
Test logged in/out, add duplicate, remove missing, zero/unlimited and full size, on-view eviction, simultaneous requests, invalid item/slug, every chosen storage backend, private/cached pages, local-storage listing and load more, post versus user versus CCT IDs, count increment/decrement/rebuild/clear, and anonymous abuse. Verify the storage medium directly as well as the rendered button.
References
- Official Data Stores overview: https://crocoblock.com/knowledge-base/articles/jetengine-data-stores-module-overview/
- Verified source paths:
wp-content/plugins/jet-engine/includes/modules/data-stores/inc/module.phpwp-content/plugins/jet-engine/includes/modules/data-stores/inc/stores/manager.phpwp-content/plugins/jet-engine/includes/modules/data-stores/inc/stores/factory.phpwp-content/plugins/jet-engine/includes/modules/data-stores/inc/stores/base.phpwp-content/plugins/jet-engine/includes/modules/data-stores/inc/stores/local-storage.phpwp-content/plugins/jet-engine/includes/modules/data-stores/inc/stores/user-ip.phpwp-content/plugins/jet-engine/includes/modules/data-stores/inc/query-builder/query.phpwp-content/plugins/jet-engine/includes/modules/custom-content-types/inc/data-stores/manager.php
Files (wp-agent-skills)
-
agents
-
openai.yaml 279 B
interface: display_name: "JetEngine Data Stores" short_description: "Build safe JetEngine Data Store integrations" default_prompt: "Use $je-data-stores to build or audit a JetEngine Data Store integration across PHP, browser storage, Query Builder, counts, and CCT items."
-
-
references
-
storage-mutation-query.md 5.3 KB
# Data Store storage, mutation, and query reference Load this reference for programmatic writes, custom store types, counter integrity, Query Builder, or CCT-backed stores. ## Factory surface Useful read/configuration methods: ```text get_slug() get_name() get_size() get_type() get_arg($name) is_user_store() is_on_view_store() can_count_posts() get_count() get_post_count($item_id) get_store() in_store($item_id) ``` Do not call `get_count()` or `in_store()` for a frontend-only local-storage Factory. Its PHP `get()` method returns `null`, while those wrappers expect an array. Branch on `$store->get_type()->is_front_store()` first. Counter methods are public but low-level: ```text increase_post_count($item_id) decrease_post_count($item_id) reset_all_post_counts() ``` Use them only after confirming a real membership transition. Replaying an add must not increment twice; removing a missing item must not decrement. ## Frontend mutation sequence Add in 3.8.14: ```text verify post_id + matching store slug read old count before-add-to-store action enforce max size; optionally evict oldest on-view item apply filtered-id(..., 'add') type->add_to_store() increment item count only if returned store count grew after-add-to-store action filter fragments; JSON success ``` Remove in 3.8.14: ```text verify post_id + matching store slug read old count apply filtered-id(..., 'remove') type->remove() before-remove-from-store action (already post-mutation) decrement item count only if returned store count shrank after-remove-from-store action filter fragments; JSON success ``` Hooks receive the original visible item ID, store slug, and Factory. Storage may receive a different ID through `filtered-id`, notably for CCT listing objects. ## Core hooks and filters ```text jet-engine/data-stores/register-store-types jet-engine/data-stores/register-stores jet-engine/data-stores/store/data jet-engine/data-stores/store-post-id jet-engine/data-stores/filtered-id jet-engine/data-stores/before-add-to-store jet-engine/data-stores/after-add-to-store jet-engine/data-stores/before-remove-from-store jet-engine/data-stores/after-remove-from-store jet-engine/data-stores/ajax-store-fragments jet-engine/data-stores/pre-get-post-count jet-engine/data-stores/custom-count-increased jet-engine/data-stores/custom-count-decreased jet-engine/data-stores/custom-reset-all-post-counts ``` Scope every global filter to the expected store slug/type. Return the incoming value unchanged for other stores. ## Custom type registration Load the subclass only from the registration callback because JetEngine loads `Base_Store` immediately before firing the action: ```php add_action( 'jet-engine/data-stores/register-store-types', static function($manager): void { require_once __DIR__ . '/src/class-my-plugin-store.php'; $manager->register_store_type(new My_Plugin_Store()); } ); ``` Contract: ```php abstract public function type_id(); abstract public function type_name(); abstract public function add_to_store($store_id, $item_id); abstract public function remove($store_id, $item_id); abstract public function get($store_id); ``` Return the resulting item count from add/remove. Make duplicate add and missing remove idempotent. Protect writes with transactions/unique constraints when the backend permits concurrent requests. `sanitize_store_item()` supports integer, string, and JSON-encoded arrays, but the custom backend must define its own round-trip shape and validation. Optional browser-only contract: ```text is_front_store() => true js_add_to_store() js_remove() js_in_store() js_get_store() ``` Treat generated JS as code: never interpolate an unescaped administrator/user value into it. ## Query Builder Type slug: `data-stores-query`. Principal settings: ```text store_slug max_items post__in (used by frontend-store bridge) post/user query filter properties ``` Server stores resolve current IDs immediately. User stores build `WP_User_Query` with `include`; post stores build `WP_Query` with `post__in` and preserve store order. A CCT bridge can return CCT row objects instead. When local storage is used, JetEngine writes an `is-front,{type},{slug}` marker and obtains actual IDs from the browser request. Enforce published/object-level visibility on server re-entry; browser IDs are untrusted. ## Counter integrity Post items use post meta `jet_engine_store_count_{slug}`. User items use the same suffix in user meta. CCT items use a generated service column whose hyphens and spaces are converted to underscores. Counts are denormalized. They drift when writes bypass Factory orchestration, when a request partially fails, or when eviction does not run a decrement path. Provide a repair/recount command for important displays. Clearing supported stores in the admin resets counts first (3.8.11+); custom backends must make clear and reset behavior complete and repeatable. ## Security review - Inspect both `wp_ajax_...` and `wp_ajax_nopriv_...` registrations. - Treat forwarded-IP headers and cookies/local storage as attacker-controlled. - Do not reveal private posts/users/CCT rows merely because their IDs are stored. - Cap store/query sizes and response cost. - Do not use global page caches for personalized server-backed store output. - Separate engagement metrics from audited business analytics.
-
-
SKILL.md 8.6 KB
--- name: je-data-stores description: >- Builds or audits JetEngine Data Store integrations for favorites, bookmarks, likes, recently viewed items, user IDs, and CCT IDs. Covers cookie, session, user-meta, local-storage, and user-IP semantics; Factory lookup; frontend and programmatic mutation; Query Builder; counters; CCT bridges; custom store types; and anonymous AJAX trust boundaries. Use when adding store buttons, querying stored items, syncing counts, extending storage, or diagnosing missing server data, spoofable state, stale counters, hooks, and size limits. 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 Data Stores Use Data Stores for preference-like collections such as favorites, bookmarks, likes, comparisons, and recently viewed items. Storage membership and counters are user-controlled signals, not authentication, authorization, payment, license, course access, or entitlement data. ## When to use this skill - Render add/remove buttons or query a configured Data Store. - Read or mutate a store from a companion plugin. - Choose among cookie, session, user metadata, local storage, and user IP. - Store user IDs or CCT item IDs instead of post IDs. - Add a custom store type. - Audit anonymous AJAX, max-size behavior, counts, caching, or synchronization. ## Module timing and lookup The Data Stores module initializes on WordPress `init` priority 0. Its configured stores are available afterward: ```php add_action('init', static function(): void { if (! class_exists('Jet_Engine\\Modules\\Data_Stores\\Module')) { return; } $module = \Jet_Engine\Modules\Data_Stores\Module::instance(); $store = $module->stores ? $module->stores->get_store('my_favorites') : false; if (! $store) { return; } $items = $store->get_store(); }, 20); ``` Allowlist the slug; do not expose arbitrary store selection from request data. `Factory::in_store()` uses non-strict `in_array()`, so normalize IDs at your boundary when type distinctions matter. ## Storage-type matrix | Type | State location | PHP-readable | Identity/reliability notes | |---|---|---:|---| | `cookies` | HttpOnly browser cookie, one-year expiry | Yes, on requests carrying it | Client-owned, size-limited by cookie constraints | | `session` | PHP session | Yes | Starts session during `parse_request`; audit full-page cache and session locking | | `user-meta` | `je_data_store_{slug}` user meta | Yes | Logged-in user only; values are normalized to strings | | `local-storage` | browser `localStorage` | No | PHP `get/add/remove` methods are empty; frontend/query bridge supplies IDs | | `user_ip` | JetEngine custom table keyed by MD5 of detected IP | Yes | IP/header-derived and collision/shared-network prone; not identity | `is_user` means the stored item IDs represent WordPress users; it does not mean the store belongs to the current user. Choose `user-meta` for per-account persistence. User stores are server-side only. ## Read path Use the configured factory: ```php $type = $store->get_type(); $type_id = $type->type_id(); $items = $type->is_front_store() ? null : (array) $store->get_store(); $contains = null === $items ? null : in_array($item_id, $items, true); $size = $store->get_size(); $can_count = $store->can_count_posts(); ``` For local storage, PHP `get_store()` returns `null`; calling the Factory `get_count()` or `in_store()` wrappers can consequently reach `count(null)` or `in_array(..., null)` on PHP 8. Use a Data Store Listing/Query Builder configuration so JetEngine's frontend bridge passes the IDs. Never silently treat a missing server result as an empty browser store. ## Mutation paths are not equivalent The frontend add/remove AJAX handlers orchestrate max size, on-view eviction, `filtered-id`, counts, fragments, and lifecycle hooks. `Factory` has no public high-level `add()`/`remove()` wrapper. Calling: ```php $store->get_type()->add_to_store($store->get_slug(), $item_id); $store->get_type()->remove($store->get_slug(), $item_id); ``` changes backend state but bypasses Factory size checks, count maintenance, fragments, and add/remove hooks. JetFormBuilder's built-in add action also calls the type directly. For programmatic writes, create one integration-owned service that validates item/actor, applies the intended size policy, maps IDs, checks an actual membership transition, updates/rebuilds counts, and emits your own idempotent domain event. Mirror JetEngine's internal AJAX sequence only when compatibility with those exact hooks is required. In 3.8.14, `before-remove-from-store` fires after the store type has already removed the value, despite its name. Do not use it to veto removal or read the pre-mutation state. Also regression-test bounded `store_on_view` plus item counters: automatic oldest-item eviction does not reconcile the evicted item's counter through the normal decrement path. ## Anonymous AJAX and trust boundary Every configured server-side store registers authenticated and `nopriv` add / remove actions. The request validates `post_id` and matching store slug, but no nonce, capability, or object-ownership check is performed. This supports public favorites/likes. Consequences: - do not use membership as proof of identity or permission; - do not use counts as fraud-resistant analytics or billing data; - validate whether an item is public/eligible in companion logic when required; - rate-limit abusive public writes outside JetEngine if counts/resources matter; - remember that a nonce would mitigate CSRF, not make a public signal trusted. The admin clear action is different: it requires `manage_options` and a valid `jet-engine-data-stores` nonce, resets counters, then clears supported storage. ## Query Builder and CCT Use Query Builder type `data-stores-query` to return stored posts or users in store order and apply supported post/user filters. Local storage is resolved on the frontend and load-more path. Set a finite `max_items`; an empty store must produce no results, not an unrestricted query. For a CCT store, enable `is_cct` and choose `related_cct`. JetEngine maps listing objects to CCT `_ID`, delegates the query to CCT Query Builder, preserves stored ID order, and stores optional counts in a generated `{store_slug}_count` CCT service column. A store cannot be both `is_user` and CCT. Test the CCT/local storage frontend bridge, filters, load more, and counter resets. ## Custom store types Register an instance extending `Jet_Engine\Modules\Data_Stores\Stores\Base_Store` on `jet-engine/data-stores/register-store-types`. Implement `type_id`, `type_name`, `add_to_store`, `remove`, and `get`. If browser-only, implement the JS methods and return true from `is_front_store()`. The manager reuses one store-type object for all configured stores and calls its `on_init()` from every Factory. Make initialization idempotent and keep per-store state keyed by slug rather than in one mutable property. Sanitize both store slug and item shape, define concurrency behavior, and implement clearing only if it is safe and complete. Read [storage-mutation-query.md](references/storage-mutation-query.md) before programmatic mutation, custom types, count-sensitive work, or CCT integration. ## Verification Test logged in/out, add duplicate, remove missing, zero/unlimited and full size, on-view eviction, simultaneous requests, invalid item/slug, every chosen storage backend, private/cached pages, local-storage listing and load more, post versus user versus CCT IDs, count increment/decrement/rebuild/clear, and anonymous abuse. Verify the storage medium directly as well as the rendered button. ## References - Official Data Stores overview: <https://crocoblock.com/knowledge-base/articles/jetengine-data-stores-module-overview/> - Verified source paths: - `wp-content/plugins/jet-engine/includes/modules/data-stores/inc/module.php` - `wp-content/plugins/jet-engine/includes/modules/data-stores/inc/stores/manager.php` - `wp-content/plugins/jet-engine/includes/modules/data-stores/inc/stores/factory.php` - `wp-content/plugins/jet-engine/includes/modules/data-stores/inc/stores/base.php` - `wp-content/plugins/jet-engine/includes/modules/data-stores/inc/stores/local-storage.php` - `wp-content/plugins/jet-engine/includes/modules/data-stores/inc/stores/user-ip.php` - `wp-content/plugins/jet-engine/includes/modules/data-stores/inc/query-builder/query.php` - `wp-content/plugins/jet-engine/includes/modules/custom-content-types/inc/data-stores/manager.php`
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.