docs
/
Building on Appmint

Search and engagement

Keyword search and reactive filters, plus the stats API behind views, likes, favourites, shares and ratings — including the three rules that make it behave.

Two things almost every catalogue or content site needs, and both have sharp edges that are not visible from the method signatures.

The backend text-searches on the keyword query parameter, across fields marked textSearch: true in the datatype.

await appmint.product.list({ keyword: 'chair' });
await appmint.product.list({ keyword: '' });      // empty = everything

An empty keyword returns the full list rather than nothing, which is what you want for a search box someone has just cleared.

Reactive filters

A grid that re-queries when a control changes does not need its own fetch code. Put the query values in runtime state, bind the grid's payload to them, and tell the grid to refresh:

// a control changes
appmint.state.set('shopKeyword', value);
document.querySelector('.shop-grid').dispatchEvent(new Event('appmint:refresh'));
<div class="shop-grid"
     data-source="product.list"
     data-source-payload='{"keyword":"{{shopKeyword}}","limit":24}'>
  <article data-bind-each>…</article>
</div>

{{shopKeyword}} interpolates from state; the appmint:refresh event makes the container re-run its source. Several controls — a keyword box, a category select, a sort — all write to different state paths and dispatch the same event.

Debounce the keyword box. Every keystroke firing appmint:refresh is a request per character, and the responses can arrive out of order — leaving the grid showing results for a prefix of what is in the box.

The stats API

Views, shares, likes, favourites, bookmarks, follows, ratings and reactions all live in one module.

POST stats/:datatype/:id/view            public, no body
POST stats/:datatype/:id/share           public, no body
POST stats/:datatype/:id/like            body { action: 'add' | 'remove' }
POST stats/:datatype/:id/dislike         body { action: 'add' | 'remove' }
POST stats/:datatype/:id/favorite        body { action: 'add' | 'remove' }
POST stats/:datatype/:id/bookmark        body { action: 'add' | 'remove' }
POST stats/:datatype/:id/follow          body { action: 'add' | 'remove' }
POST stats/:datatype/:id/rating          body { action: 'add' | 'update', value: number }
POST stats/:datatype/:id/reaction        body { action, value: string }
GET  stats/:datatype/:id                 → summary

From the runtime:

appmint.activity.view(datatype, id);
appmint.activity.like(datatype, id, 'add');
appmint.activity.favorite(datatype, id, 'remove');
appmint.activity.rate(datatype, id, 4);
const summary = await appmint.activity.summary(datatype, id);
const mine    = await appmint.activity.mine(datatype, id);

A summary:

{
  "likes": 42, "views": 1980, "shares": 7,
  "bookmarks": 3, "favorites": 12, "averageRating": 4.3,
  "user": { "liked": true, "favorited": false, "bookmarked": false }
}

Three rules

id is the record's sk, not your business key. The server does new ObjectId(id). Pass a SKU or a slug and it fails — the id is the Mongo id.

Everything except view and share needs a signed-in customer. Those two are public and unauthenticated, which is what makes a view counter possible on an anonymous page. The rest return 401 for a guest, so gate the UI on sign-in rather than letting someone click a heart that cannot work.

There is no auto-toggle. favorite with action: 'add' on something already favourited is a no-op, not an un-favourite. To build a toggle you must know the current state first:

const s = await appmint.activity.summary('sf_product', id);
const next = s.user.favorited ? 'remove' : 'add';
await appmint.activity.favorite('sf_product', id, next);

Reading summary.user before every toggle is a request per click. Read it once when the page loads, hold the flag locally, and flip it optimistically on click — reconciling only if the call fails. Otherwise a fast double-click sends two adds and the heart sticks.

Counting views

<article data-action="activity.view"
         data-payload='{"datatype":"post","id":"…"}'
         data-trigger="load">

data-trigger accepts load and inview. inview fires when the element scrolls into the viewport, which is the more honest measure for anything below the fold — a "view" for an article nobody scrolled to is not a view.

These counters are $inc operations with no deduplication. A reload counts again. If you need unique views, that is a different problem and this is not the tool for it.