Two things almost every catalogue or content site needs, and both have sharp edges that are not visible from the method signatures.
Search
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 = everythingAn 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 → summaryFrom 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.
Related
- Dynamic content — rendering the rows these act on
- Configurable products — what a product row contains
- Actions reference — the full namespace list