Claude Skill

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;

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download lonsdale201-wp-agent-skills-jet-engine_je-data-stores-52f6020.zip · 6 KB
Part of lonsdale201/wp-agent-skills — 226 skills

Install

skills CLI npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/jet-engine/je-data-stores
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart
Git 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.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
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.

No comments yet.

Reviews (0)

No reviews yet.

Related