@trap_stevo/ventry
Version:
The universal engine for creating, tracking, and evolving interactive content β from posts, comments, and likes to offers, auctions, events, and beyond. Define, extend, and analyze content objects in real time. Turn anything into user-driven content.
889 lines (694 loc) β’ 42.1 kB
Markdown
# π @trap_stevo/ventry
**Turn anything into user-driven content.**
The universal engine for creating, tracking, and evolving interactive content β from posts, comments, and likes to offers, auctions, events, and beyond.
Define, extend, and analyze content objects in real time.
---
## π Features
* π **Universal Content Engine** β Create, update, and remove *any* content type dynamically
* π§© **Type Registry** β Define & evolve content types with custom visibility policies
* π¬ **Interactions Out of the Box** β Comments, reactions, likes, favorites, reports
* πΈ **Commerce Ready** β Offers, bids, auctions, settlements
* π **Integrated Metrics** β Visits, favorites, likes, reports, top items, time series
* π **Eventing & Scheduling** β Emitted item lifecycle events + expiration scheduler
* π **Geo / Deployment Filters** β Region-aware visibility (allow/deny lists)
* β‘ **Feeds & Ranking** β Built-in scoring or pluggable ranking callbacks
---
## βοΈ System Requirements
| Requirement | Version |
| ----------- | --------------------- |
| **Node.js** | β₯ 18.x |
| **npm** | β₯ 9.x (recommended) |
| **OS** | Windows, macOS, Linux |
---
# βοΈ Configuration
### π§ Constructor
```js
new Ventry(options?: VentryOptions)
```
#### `VentryOptions` (Top-Level)
| Key | Type | Default | Description |
|--------------------------|--------------------------|----------|-------------|
| `contentVault` | `StarVault \| string \| null` | β | Existing Star-Vault instance **or** a path/string to trigger internal creation (see `vaultOptions`). |
| `vaultOptions` | `object` | `{}` | Options passed to internal `ContentVaultInstanceManager` (and used to construct Star-Vault when `contentVault` is not provided). |
| `contentMetrics` | `MetricTide \| string \| null` | β | Existing MetricTide instance **or** options for internal creation (see `metricOptions`). |
| `metricOptions` | `object` | `{}` | Options passed to internal Contalytics instance. |
| `vaultActionInfoDefault` | `object` | `{}` | Default `actionInfo` metadata (propagated into vault calls). |
| `vaultClientAuthDefault` | `object \| null` | `null` | Default `clientAuth` context for vault operations. |
| `metricPrefix` | `string` | `"ugc"` | Prefix for metric names (e.g., `ugc.visit`, `ugc.like`). |
| `eventPrefix` | `string` | `"ugc"` | Prefix for emitted events. |
| `autoStartScheduler` | `boolean` | `true` | Auto-start expiration scheduler on init. |
---
## ποΈ Content Vault (Star-Vault) via `vaultOptions`
If you **donβt** pass an existing `contentVault` instance, Ventry will construct one with these [Star-Vault](https://www.npmjs.com/package/@trap_stevo/star-vault) parameters:
```js
new StarVault(
dbPath, // default: "./Ventry"
logPath, // default: "./Ventry_Logs"
shardCount, // default: 4
maxLogSize, // default: "869MB"
logRetention, // default: "1w"
options // default: {}
)
```
### `vaultOptions` (Ventry)
| Key | Type | Default | Used For |
|----------------|-----------|----------------|---------|
| `dbPath` | `string` | `"./Ventry"` | Star-Vault constructor `dbPath`. |
| `logPath` | `string` | `"./Ventry_Logs"` | Star-Vault constructor `logPath`. |
| `shardCount` | `number` | `4` | Star-Vault `shardCount`. |
| `maxLogSize` | `string` | `"869MB"` | Star-Vault `maxLogSize`. |
| `logRetention` | `string` | `"1w"` | Star-Vault `logRetention`. |
| `options` | `object` | `{}` | Star-Vault low-level options (permissions, encryption, auth, etc.). |
| `prefix` | `string` | `"ugc"` | **Collection key prefix** used by Ventry: items stored under `${prefix}-items`, types under `${prefix}-types`, etc. |
**Collections used (derived from `prefix`):**
- `${prefix}-types`
- `${prefix}-items`
- `${prefix}-comments`
- `${prefix}-offers`
- `${prefix}-auctions`
- `${prefix}-bids`
---
## π Metrics (MetricTide) via `metricOptions`
If you **donβt** pass an existing `contentMetrics` instance, Ventry will construct one with these [MetricTide](https://www.npmjs.com/package/@trap_stevo/metrictide) parameters:
```js
new MetricTide({
dbPath: "./.ventry-metrics",
persistence: { alwaysOn: true },
loadMetricsOnLaunch: true,
// ...your metricOptions (merged)
})
```
### `metricOptions` (Ventry)
| Key | Type | Default | Description |
|-------------------|----------------------|-----------------------|-------------|
| `dbPath` | `string` | `"./.ventry-metrics"` | Storage for metrics (MetricTide). |
| `persistence` | `object` | `{ alwaysOn: true }` | MetricTide persistence config. |
| `loadMetricsOnLaunch` | `boolean` | `true` | Hydrate metrics on boot. |
| `metricPrefix` | `string` | **from Ventry** | Prefix applied to tracked metrics (e.g., `ugc.like`). |
| *(any MetricTide option)* | `any` | β | Forwarded to MetricTide constructor. |
---
# π§ Content Types
Types let you define how a class of content behaves (posts, listings, experiences, etc.). You can attach visibility policies, defaults, validation rules, allowed statuses, indexing hints, and branch-specific subvault behaviorβwithout changing your item schema.
> Register types early at boot. You can update any field later with `updateType`.
## π§ Registering a Type
```js
const { Ventry } = require("@trap_stevo/ventry");
const v = new Ventry({ /* ...options... */ });
v.registerType({
typeID : "post",
// Identity & docs
title : "User Post",
description : "Short text posts with optional media attachments",
// Default fields applied to new items of this type
defaults : {
status : "active",
deployment : { scope : "global" },
commentsEnabled : true,
data : {
maxChars : 280
},
labels : [],
tags : []
},
// Validation run on create/update (throw string to reject)
validate : ({ record, phase }) => {
if (!record.data || typeof record.data.text !== "string") {
throw "post.data.text is required";
}
if (record.data.text.length > (record.data.maxChars || 280)) {
throw "text exceeds maxChars";
}
},
// Allowed lifecycle states for this type
allowedStatuses : [ "active", "hidden", "archived", "banned" ],
// Visibility policy used by feed/query (overrides default scoring if returns a number)
// Return a number for rank score, or a boolean to allow/deny.
visibilityPolicy : ({ item, metrics, boosts, geoContext, visibilityContext, now }) => {
// Example: deny if explicitly flagged
if (item.data.labels?.includes("flagged")) { return false; }
// Example: soft boost if "featured"
const base = (metrics.visits24h || 0) + (metrics.likes7d || 0) * 4 + (metrics.favorites7d || 0) * 5;
const isFeatured = item.data.labels?.includes("featured");
return isFeatured ? base * 1.25 : base;
},
// Indexing hints (forwarded to the underlying vault)
// Useful keys to accelerate queries
indexes : [
{ key : "ownerID" },
{ key : "status" },
{ key : "tags" },
{ key : "timestamp" }
],
// Subvault branches commonly used with this type (optional, declarative)
// Purely a convention aid; you can still use any branch name ad-hoc.
branches : {
"post_Reactions" : { ttlMs : 0 },
"post_Moderation" : { ttlMs : 0 }
}
});
```
## Updating a Type
```js
v.updateType("post", {
description : "Short text posts (β€ 280 chars) with optional media",
allowedStatuses : [ "active", "hidden", "archived", "banned", "locked" ],
defaults : {
commentsEnabled : false
}
});
```
---
## π§© `TypeConfig` Shape
| Field | Type | Required | Description |
|------|------|----------|-------------|
| `typeID` | `string` | **Yes** | Unique identifier for the type. |
| `title` | `string` | No | Human-friendly name. |
| `description` | `string` | No | What this type represents. |
| `defaults` | `object` | No | Default fields merged into new items of this type. Supports any item fields (`status`, `deployment`, `expiration`, `boosts`, `commentsEnabled`, `data`, `tags`, `labels`, `attachments`). |
| `validate` | `function({ record, phase })` | No | Throw a string to reject; receives `{ phase : "create" \| "update" }`. |
| `allowedStatuses` | `string[]` | No | Whitelist of legal statuses for items of this type. |
| `visibilityPolicy` | `function(ctx)` | No | Custom ranking / gate. Return `false` to hide, `true` to allow (use default scoring), or `number` to override score. |
| `indexes` | `{ key : string }[]` | No | Index hints passed to the vault. |
| `branches` | `Record<string, { ttlMs? : number }>` | No | Common subvault branches (convention only). |
> β οΈ If `validate` throws, `create`/`update` will fail. Keep messages user-readable.
---
## π― Important Props You Can Control Per Type
These are item-level fields that your **type defaults** and **validation** often manage:
- `status` β One of your `allowedStatuses`. Common: `"active"`, `"hidden"`, `"archived"`, `"banned"`, `"locked"`.
- `deployment` β `{ scope : "global" \| "regional", geo? : { allow? : string[], deny? : string[] } }`.
- `expiration` β `{ data : { ttlMs? : number, expiresAt? : number }, onExpire : "hide" \| "archive" \| "delete" }`.
- `boosts` β `[{ weight : number, startsAt? : number, endsAt? : number }]` used by ranking.
- `commentsEnabled` β `boolean` to toggle comment capability.
- `labels` / `tags` β Freeform classification for moderation, ranking, search.
- `attachments` β Array of attachment descriptors you define (URLs, blobs, content IDs).
- `data` β Arbitrary payload. Put your domain fields here and validate in `validate`.
---
## π Status & Moderation
Enforce status constraints in your `validate`:
```js
v.registerType({
typeID : "listing",
allowedStatuses : [ "draft", "active", "sold", "archived", "banned" ],
validate : ({ record, phase }) => {
const s = record.status;
if (s && ![ "draft", "active", "sold", "archived", "banned" ].includes(s)) {
throw "invalid status for listing";
}
if (s === "sold" && typeof record.data?.price !== "number") {
throw "sold listings must include final price in data.price";
}
}
});
```
---
## π Geo & Deployment Controls
- **Global** scope shows everywhere.
- **Regional** scope applies `deny` first, then `allow`, else fallback allow.
```js
v.registerType({
typeID : "event",
defaults : {
deployment : {
scope : "regional",
geo : { allow : [ "US", "CA" ], deny : [ "PR" ] }
}
}
});
```
---
## β³ Expiration Policies (Per Type Defaults)
```js
v.registerType({
typeID : "story",
defaults : {
expiration : {
data : { ttlMs : 24 * 60 * 60 * 1000 },
onExpire : "delete"
}
}
});
```
---
## β‘ Boosts & Ranking
Use `boosts` + `visibilityPolicy` to shape ranking windows:
```js
v.registerType({
typeID : "announcement",
visibilityPolicy : ({ item, metrics, now }) => {
const within24h = (now - (item.timestamp || now)) < 86_400_000;
const base = (metrics.visits24h || 0) + (metrics.likes7d || 0) * 3;
return within24h ? base * 2 : base;
}
});
```
---
## π§΅ Subvault Branch Conventions
Declare common branches as documentation (you can still pass any `branch` at call-time):
```js
v.registerType({
typeID : "experience",
branches : {
"Experience_Stats" : {},
"Experience_Sessions" : {}
}
});
// Usage
v.upsertSubvaultByKey(expId, userId, "lifetime", { hours : 12 }, {}, null, "Experience_Stats");
```
---
## π Indexing Hints
Index keys you frequently filter on (e.g., `status`, `ownerID`, `typeID`, `timestamp`, `tags`).
```js
v.registerType({
typeID : "photo",
indexes : [
{ key : "ownerID" },
{ key : "status" },
{ key : "labels" },
{ key : "timestamp" }
]
});
```
---
## β
Registering a Type
```js
v.registerType({
typeID : "market_listing",
title : "Marketplace Listing",
description : "Buy/sell listings with ask price and inventory",
allowedStatuses : [ "draft", "active", "paused", "sold", "archived", "banned" ],
defaults : {
status : "draft",
commentsEnabled : true,
deployment : { scope : "global" },
data : {
currency : "USD",
inventory : 1
},
labels : [],
tags : []
},
validate : ({ record, phase }) => {
const d = record.data || {};
if (phase === "create" && typeof d.title !== "string") {
throw "listing.data.title is required";
}
if (typeof d.price !== "number" || d.price <= 0) {
throw "listing.data.price must be a positive number";
}
if (!Number.isInteger(d.inventory) || d.inventory < 0) {
throw "listing.data.inventory must be a non-negative integer";
}
if (record.status === "sold" && d.inventory !== 0) {
throw "sold listings must have inventory 0";
}
},
visibilityPolicy : ({ item, metrics }) => {
// Promote fresh, popular, reasonably priced items
const price = item.data.data?.price || 0.50;
const w = price > 0 ? Math.min(1.0, 250 / price) : 1.0;
return ((metrics.visits24h || 0) + (metrics.likes7d || 0) * 3 + (metrics.favorites7d || 0) * 4) * w;
},
indexes : [
{ key : "status" },
{ key : "ownerID" },
{ key : "tags" },
{ key : "timestamp" }
],
branches : {
"Listing_Offers" : {},
"Listing_Audit" : {}
}
});
// Create with type defaults + validation
const { id } = await v.create("seller-42", {
typeID : "market_listing",
data : { title : "Gaming Laptop", price : 1299.00, inventory : 3 },
tags : [ "electronics", "laptop" ]
});
// Move to active (allowed by type)
await v.update(id, { id : "seller-42", strictOwner : true }, { status : "active" });
```
---
# π API Specifications
## π§ Core Methods
| Method | Signature | Returns | Description | Async/Sync |
| ----------------------------- | --------------------------------------------------------- | -------------------- | -------------------------------------------------------- | ---------- |
| `create(ownerID, payload, actionInfo?)` | `(string, object, object?)` | `{ id } \| null` | Creates a new content item. | **Async** |
| `getItems(actionInfo?)` | `(object?)` | **StarVaultQueryBuilder** | Opens a chainable query on the `items` collection. Call `.execute(true|false)` to get results. | **Sync** |
| `get(id, actionInfo?)` | `(string, object?)` | `object \| null` | Retrieves an item by ID. | **Async** |
| `update(id, actor, patch, actionInfo?)` | `(string, object, object, object?)` | `object \| null` | Updates an item with patch semantics. | **Async** |
| `remove(id, actor, opts?, actionInfo?)` | `(string, object, { hard?: boolean }, object?)` | `object \| null` | Soft- or hard-deletes an item. | **Async** |
| `getContentRecordHistory(collectionID, id, options?, actionInfo?, clientAuth?)` | `(string, string, object?, object?, object?)` | `Array<object>` | History for a specific record in a collection. | **Sync** |
| `getContentRecordTimeline(collectionID, id, options?, actionInfo?, clientAuth?)` | `(string, string, object?, object?, object?)` | `Array<object>` | Timeline (ordered events) for a specific record. | **Sync** |
| `getContentHistory(collectionID, options?, actionInfo?, clientAuth?)` | `(string, object?, object?, object?)` | `Array<object>` | History across a collection (items, types, etc.). | **Sync** |
| `getContentTimeline(collectionID, options?, actionInfo?, clientAuth?)` | `(string, object?, object?, object?)` | `Array<object>` | Timeline view across a collection. | **Sync** |
| `getHistory(options?, actionInfo?, clientAuth?)` | `(object?, object?, object?)` | `Array<object>` | Vault-wide history inspection across collections. | **Sync** |
| `syncHistory(actionInfo?, clientAuth?)` | `(object?, object?)` | `object` | Forces history/log sync (e.g. compaction, ingestion). | **Async** |
| `registerType(config, actionInfo?)` | `(object, object?)` | `object \| null` | Registers or upserts a content type. | **Sync** |
| `updateType(typeID, patch, actionInfo?)` | `(string, object, object?)` | `object \| null` | Patches a registered content type. | **Sync** |
| `listTypes(actionInfo?)` | `(object?)` | `Array<object>` | Lists all registered content types. | **Sync** |
| `feed(params)` | `(object)` | `{ items, nextCursor }` | Ranked feed with optional custom scoring. | **Sync** |
| `query(params)` | `(object)` | `{ items, nextCursor }` | Flexible item queries. | **Sync** |
| `trackItemMetric(itemID, metric, value?, actorID?, extras?, actionInfo?)` | `(string, string, number?, string?, { tags?: object, metadata?: object }?, object?)` | `{ ok: boolean, reason?: string }` | **Universal metric tracker**. Auto-tags with `itemID`, `typeID`, `ownerID`, optional `actorID`; merges `extras.tags`; passes `extras.metadata`. Emits `item.metric` and `item.metric.<metric>`. | **Sync** |
| `trackItemMetrics(batch, actionInfo?)` | `({ itemID: string, metric: string, value?: number, actorID?: string, extras?: { tags?: object, metadata?: object } }[], object?)` | `{ ok: boolean, count: number }` | Batch version of `trackItemMetric`. Processes all records; returns count of successful writes. | **Sync** |
| `visit(id, actorID, ctx?)` | `(string, string, object?)` | `void` | Tracks a visit metric, emits `item.visit`. | **Sync** |
| `like(id, actorID)` | `(string, string)` | `void` | +1 like (metric), emits `item.like`. | **Sync** |
| `unlike(id, actorID)` | `(string, string)` | `void` | β1 like (metric), emits `item.unlike`. | **Sync** |
| `favorite(id, actorID)` | `(string, string)` | `void` | +1 favorite (metric), emits `item.favorite`. | **Sync** |
| `unfavorite(id, actorID)` | `(string, string)` | `void` | β1 favorite (metric), emits `item.unfavorite`. | **Sync** |
| `reportItem(id, reporter, reason, meta?)` | `(string, string, any, any?)` | `void` | Tracks a report metric, emits `item.report`. | **Sync** |
| `addComment(itemID, ownerID, payload)` | `(string, string, object)` | `object` | Adds a comment via `CommentsManager`. | **Sync** |
| `listComments(itemID, params?)`| `(string, object?)` | `Array<object>` | Lists comments. | **Sync** |
| `reactToComment(id, actorID, rt)` | `(string, string, string)` | `object` | Reacts to a comment. | **Sync** |
| `reportComment(id, reporter, reason, meta?)` | `(string, string, any, any?)` | `object` | Reports a comment. | **Sync** |
| `proposeOffer(itemID, bidderID, input)` | `(string, string, object)` | `object` | Offer lifecycle (OffersManager). | **Sync** |
| `acceptOffer(offerID, ownerID)` | `(string, string)` | `object` | Accepts an offer. | **Sync** |
| `rejectOffer(offerID, ownerID, reason)` | `(string, string, any)` | `object` | Rejects an offer. | **Sync** |
| `withdrawOffer(offerID, bidderID)` | `(string, string)` | `object` | Withdraws an offer. | **Sync** |
| `listOffers(itemID, filter?, page?)` | `(string, any?, any?)` | `Array<object>` | Lists offers. | **Sync** |
| `markItemSold(itemID, ownerID, sale)` | `(string, string, object)` | `object` | Marks an item sold. | **Sync** |
| `toggleForSale(itemID, ownerID, forSale)` | `(string, string, boolean)` | `object` | Toggles listing state. | **Sync** |
| `updatePrice(itemID, ownerID, price)` | `(string, string, number)` | `object` | Updates listing price. | **Sync** |
| `startAuction(itemID, ownerID, cfg)` | `(string, string, object)` | `object` | Starts an auction. | **Sync** |
| `cancelAuction(auctionID, ownerID, reason)` | `(string, string, any)` | `object` | Cancels an auction. | **Sync** |
| `getAuction(auctionID)` | `(string)` | `object \| null` | Reads an auction. | **Sync** |
| `listAuctions(filter?, page?)`| `(object?, any?)` | `Array<object>` | Lists auctions. | **Sync** |
| `watchAuction(auctionID, actorID)` | `(string, string)` | `object` | Watch an auction. | **Sync** |
| `unwatchAuction(auctionID, actorID)` | `(string, string)` | `object` | Unwatch an auction. | **Sync** |
| `placeBid(auctionID, bidderID, input)` | `(string, string, object)` | `object` | Places a bid. | **Sync** |
| `retractBid(bidID, bidderID, reason)` | `(string, string, any)` | `object` | Retracts a bid. | **Sync** |
| `listBids(auctionID, page?)` | `(string, any?)` | `Array<object>` | Lists bids. | **Sync** |
| `getLeadingBid(auctionID)` | `(string)` | `object \| null` | Gets current leader. | **Sync** |
| `endAuction(auctionID, systemActor)` | `(string, object)` | `object` | Ends an auction. | **Sync** |
| `settleAuction(auctionID, ownerID, settlement)` | `(string, string, object)` | `object` | Settles an auction. | **Sync** |
| `getAllAnalytics(opts?)` | `(object?)` | `Array<object>` | All metrics (since window / filters). | **Sync** |
| `getItemAnalytics(itemID, opts?)` | `(string, object?)` | `Array<object>` | Raw metrics for an item. | **Sync/Async*** |
| `getItemAnalyticsSummary(itemID, opts?)` | `(string, object?)` | `object` | Aggregated counts for an item. | **Async** |
| `getItemMetricTimeSeries(itemID, metric, opts?)` | `(string, string, object?)` | `object` | Metric time series (delegates to MetricTide). | **Sync** |
| `getTopItemsBy(metric, opts?)`| `(string, object?)` | `Array<{ itemID, total }>` | Top items by a metric. | **Async** |
\* `getItemAnalytics` performs an in-memory scan, and optionally augments with persisted βsinceβ lookups via MetricTide if `since` is provided (async path).
---
## π§ Record History & Timeline
Ventry reconstructs **record history** and a human-readable **timeline** from write-ahead logs. These calls support filtering, integrity checks, decryption-aware output, and optional auditing.
### `options` (history/timeline)
| Key | Type | Default | Notes |
|---|---|---|---|
| `includeArchived` | boolean | `true` | Include rotated `*.archived` segments. |
| `verifyChecksums` | boolean | `true` | Verify SHA-256 checksums of entries; skip failures. |
| `includeSystem` | boolean | `true` | Merge related system-level events (DELETE, TX markers). |
| `actions` | string \| string[] | `undefined` | Allow-list: e.g., `"UPDATE"` or `["WRITE","UPDATE"]`. Case-insensitive. |
| `excludeActions` | string \| string[] | `undefined` | Block-list: e.g., `["DELETE"]`. Case-insensitive. |
| `since` | number \| string \| Date | `undefined` | Only include entries at/after this timestamp. |
| `until` | number \| string \| Date | `undefined` | Only include entries at/before this timestamp. |
| `match` | `(row) => boolean` | `undefined` | Custom predicate applied after other filters. |
| `includeDecrypted` | boolean | auto | When true and encryption is enabled, decrypt entries and populate `dataDecrypted`. |
| `audit` | boolean | `undefined` | Enables per-call auditing; see βAuditingβ. |
> History entries include `source`, `collection`, `action`, `id`, `timestamp`, `client`, `data`, `checksum`, and when present: `eventID`, `transactionID`, `idempotencyKey`. With `includeDecrypted`, output may contain `dataEncrypted` and `dataDecrypted`.
---
### History Examples
```js
// Full history (archived segments, checksum verification)
const history = v.getContentRecordHistory("users/region1", "1760233994625", {
includeArchived : true,
verifyChecksums : true
}, { actor : "admin-1" }, { token : "valid" });
// Windowed, updates only
const updates = v.getContentRecordHistory("users/region1", "1760233994625", {
actions : "UPDATE",
since : Date.now() - 60_000,
until : Date.now()
});
// Compact timeline, no deletions, explicit audit
const timeline = v.getContentRecordTimeline("users/region1", "1760233994625", {
excludeActions : ["DELETE", "SOFT_DELETE"]
}, { audit : true }, { token : "valid" });
// Collection-level history
const collectionHistory = v.getContentHistory("users/region1", {
includeArchived : true,
includeSystem : true
});
// Vault-wide history with collection filtering
const vaultHistory = v.getHistory({
collections : ["users/*", "orders/*"],
actions : ["CREATE", "UPDATE"]
});
// Synchronous WAL flush before backup/export
await v.syncHistory({}, { token : "valid" });
```
---
## π§Ύ Auditing (History & Timeline Reads)
History/timeline reads can write entries to `audit.log` for compliance, governance, or access tracking.
Auditing activates when **any** of the following are true:
1. `options.audit === true`
2. `actionInfo.audit === true`
3. Auditing enabled globally in vault initialization
Each audited read emits:
- `action` : `"HISTORY_READ"` or `"TIMELINE_READ"`
- `details` : `{ kind, collection, id, count, filters }`
- `filters` is sanitized (booleans, timestamps, allow/block lists, and a flag if `match` is used)
### Per-call example
```js
v.getContentRecordHistory("orders", "o-123", { audit : true });
```
### Default auditing example
```js
const v = new Ventry({
vaultOptions : {
options : {
auditHistory : true
}
}
});
```
### Audit action names
- `HISTORY_READ`
- `TIMELINE_READ`
These cover record-level, collection-level, and global history/timeline reads.
---
## β οΈ Privacy Notes
- If handling sensitive material, prefer `includeDecrypted : false` unless absolutely required.
- For observability/logging, prefer logging **IDs** and **checksums**, not full decrypted data.
- Timeline output is intentionally compact; use full history mode for migrations or forensic analysis.
---
## π₯ Content Imports
Ventry exposes structured, high-integrity content import helpers for migrating data, seeding environments, snapshot restoration, collection cloning, and bulk ingestion pipelines.
All import functions support per-call `options`, `actionInfo`, and `clientAuth`.
### Methods
| Method | Signature | Returns | Description | Sync/Async |
|-------|-----------|----------|-------------|------------|
| `importContentRecord(collectionID, record, options?, actionInfo?, clientAuth?)` | `(string, object, object?, object?, object?)` | `object` | Imports a single record into a collection. Validates structure, preserves timestamps, and applies import-mode semantics. | **Sync** |
| `importManyContentRecords(collectionID, records, options?, actionInfo?, clientAuth?)` | `(string, object[], object?, object?, object?)` | `{ imported, failed }` | Bulk import for many records. Performs batched write-ahead logging and integrity checks. | **Sync** |
| `importContent(collectionID, records, options?, actionInfo?, clientAuth?)` | `(string, any, object?, object?, object?)` | `{ imported, failed }` | Flexible import accepting arrays, maps, or normalized payloads. Best for ETL pipelines or heterogeneous migrations. | **Sync** |
| `importSnapshot(snapshot, options?, actionInfo?, clientAuth?)` | `(object, object?, object?, object?)` | `{ collections, count }` | Restores a full snapshot of collections/items/types into the vault. Supports filtered restore, ID remapping, and migration guards. | **Sync** |
---
### Notes on Import Behavior
- Import operations bypass typical lifecycle flows (e.g., ranking, comments, reactions) unless explicitly included in the payload.
- Timestamps (`createdAt`, `updatedAt`) are preserved when provided.
- ID collisions can be handled via `options` (overwrite, skip, or error).
- Import obeys all encryption/decryption behavior automatically.
- Ideal for:
- Initial seeding
- Local & CI environment setup
- Migrating between environments
- Restoring from backups
- Cloning tenants/regions/apps
- Reconstructing data for replay/testing
---
### Example: Import One Record
```js
v.importContentRecord("items", {
id : "post-001",
ownerID : "user-123",
typeID : "post",
data : { text : "Migrated post" },
timestamp : 1700000000000
}, {
overwrite : true
}, { source : "migration" }, { token : "system" });
```
---
### Example: Bulk Import
```js
const result = v.importManyContentRecords("items", [
{ id : "a", ownerID : "u1", typeID : "post", data : { text : "Hello" } },
{ id : "b", ownerID : "u2", typeID : "post", data : { text : "World" } }
], {
skipExisting : true
});
```
---
### Example: Snapshot Import
```js
// A snapshot may contain multiple collections.
const snapshot = {
"ugc-items" : [{ id : "1", ownerID : "x", typeID : "post", data : { text : "Imported" } }],
"ugc-types" : [{ typeID : "post", defaults : { commentsEnabled : true } }]
};
v.importSnapshot(snapshot, {
collections : ["ugc-items", "ugc-types"]
}, { source : "snapshot_restore" }, { token : "system" });
```
---
## π§© Item Subvaults
### π§ Subvault Methods
Items support *nested, branch-aware* data for structured, isolated extensions integrating seamlessly into storage, querying, and event workflows without altering the core item model.
These methods operate on the `subvault` branch by default, but you can provide a custom `branch` (e.g. `"Experience_Stats"`).
| Method | Signature | Returns | Description | Async/Sync |
|---------|------------|----------|--------------|-------------|
| `addSubvault(itemID, ownerID, input?, actionInfo?, branch?)` | `(string, string, object?, object?, string?)` | `{ id }` | Adds a new Subvault record under an item. Emits `subvault.created`. | **Sync** |
| `listSubvault(itemID, opts?, actionInfo?, branch?)` | `(string, object?, object?, string?)` | `{ items, nextCursor }` | Lists all Subvault entries for a given parent item. | **Sync** |
| `getSubvaultByKey(itemID, key, actionInfo?, branch?)` | `(string, string, object?, string?)` | `object \| null` | Retrieves a Subvault record by `key` under the given item. | **Sync** |
| `upsertSubvaultByKey(itemID, ownerID, key, payload?, extras?, actionInfo?, branch?)` | `(string, string, string, object?, object?, object?, string?)` | `object` | Creates or updates a Subvault record under the same `key`. Emits `subvault.upserted`. | **Sync** |
| `updateSubvault(subvaultID, actor, patch?, actionInfo?, branch?)` | `(string, object, object?, object?, string?)` | `object \| null` | Updates a Subvault entry. Performs patch semantics internally. Emits `subvault.updated`. | **Sync** |
| `removeSubvault(subvaultID, actor, opts?, actionInfo?, branch?)` | `(string, object, { hard?: boolean }?, object?, string?)` | `object` | Deletes a Subvault (soft by default). Emits `subvault.deleted_soft` or `subvault.deleted_hard`. | **Sync** |
| `existsSubvaultKey(itemID, key, actionInfo?, branch?)` | `(string, string, object?, string?)` | `boolean` | Returns whether a Subvault entry exists for the given key. | **Sync** |
| `countSubvault(itemID, filter?, actionInfo?, branch?)` | `(string, object?, object?, string?)` | `number` | Counts Subvault entries matching a filter. | **Sync** |
| `listSubvaultKeys(itemID, filter?, actionInfo?, branch?)` | `(string, object?, object?, string?)` | `Array<string>` | Returns all keys under the specified item. | **Sync** |
| `moveSubvault(subvaultID, toItemID, actionInfo?, branch?)` | `(string, string, object?, string?)` | `object` | Moves a Subvault record to another parent. Emits `subvault.moved`. | **Sync** |
| `cloneSubvaultRecords(fromItemID, toItemID, options?, actionInfo?, fromBranch?, toBranch?)` | `(string, string, object?, object?, string?, string?)` | `{ created }` | Clones all or selected Subvault records between items. Emits `subvault.cloned`. | **Sync** |
| `touchSubvault(subvaultID, actionInfo?, branch?)` | `(string, object?, string?)` | `object` | Updates `updatedAt` timestamp. | **Sync** |
| `setSubvaultStatus(subvaultID, status, actionInfo?, branch?)` | `(string, string, object?, string?)` | `object` | Updates the Subvault recordβs status. | **Sync** |
| `purgeExpiredSubvault(itemID?, actionInfo?, branch?)` | `(string?, object?, string?)` | `{ deleted }` | Removes expired Subvault records (`expiresAt <= now`). | **Sync** |
---
### π Subvault Events
The following events are emitted automatically through Ventryβs event system:
| Event | Trigger |
|--------|----------|
| `subvault.created` | When a new Subvault record is added. |
| `subvault.updated` | When a Subvault is modified. |
| `subvault.upserted` | When a Subvault is created or updated by key. |
| `subvault.deleted_soft` | When a Subvault is soft-deleted. |
| `subvault.deleted_hard` | When a Subvault is hard-deleted. |
| `subvault.moved` | When a Subvault is moved to another parent. |
| `subvault.cloned` | When Subvault records are cloned between items. |
---
# π Events & Scheduler
## Events API (from `EventsManager`)
| Method | Description |
|-------------------------|-------------|
| `on(event, fn)` | Subscribe to Ventry events. |
| `once(event, fn)` | One-shot subscription. |
| `off(event, fn)` | Unsubscribe. |
| `setEventPrefix(prefix)`| Change runtime event prefix. |
| `configureScheduler(cfg)`| Configure expiration scheduler. |
| `startScheduler()` / `stopScheduler()` | Control scheduler loop. |
**Item lifecycle events emitted by Ventry** (prefix configurable):
- `item.created`
- `item.updated`
- `item.deleted_soft`
- `item.deleted_hard`
- `item.visit`
- `item.like` / `item.unlike`
- `item.favorite` / `item.unfavorite`
- `item.report`
---
## π Deployment & Geo Visibility
Items can define:
```js
deployment: {
scope: "global",
geo: {
allow: ["US", "CA"],
deny: ["CN"]
}
}
```
At query/feed time Ventry applies denyβallowβfallback logic.
Pass `geoContext` to `query`/`feed` to enable filtering.
---
## β³ Expiration Policies
Items can define:
```js
expiration: {
data: { ttlMs: 86400000 },
onExpire: "archive"
}
```
Scheduler will hide/archive/delete expired items.
---
## π Feed Ranking
Default score:
```
score = ((visits24h + likes7d*4 + favorites7d*5) * (1 + boostSum)) / ageHours^1.15
```
Provide `onRanking({ item, metrics, boosts, now })` for custom ranking.
---
# π¦ Installation
```bash
npm install @trap_stevo/ventry
```
---
## π οΈ Usage
```js
const { Ventry } = require("@trap_stevo/ventry");
const v = new Ventry({
vaultOptions: {
prefix : "myapp",
dbPath : "./MyApp",
logPath : "./MyApp_Logs",
options : {
enableEncryption : false,
authHandler : (auth) => {
return auth.token === "any-token";
}
}
},
metricOptions: {
dbPath: "./.myappalytics"
},
metricPrefix: "myapp",
eventPrefix: "myapp",
vaultActionInfoDefault : { source : `myapp_core_${Date.now()}` },
vaultClientAuthDefault : { token : "any-token" }
});
// Create an item
const { id } = await v.create("user-123", {
typeID: "post",
data: { text: "Hello, world!" },
commentsEnabled: true
});
// Act on it
v.like(id, "user-456");
v.visit(id, "user-789", { ref: "home" });
// Query a feed
const feed = v.feed({ typeID: "post", limit: 10 });
console.log(feed.items);
```
---
## β¨ Example: Rich Create
```js
const { id } = await v.create("seller-1", {
typeID: "listing",
data: { title: "Concert tickets", price: 120 },
tags: ["tickets", "event"],
deployment: { scope: "regional", geo: { allow: ["US"] } },
expiration: { data: { ttlMs: 86400000 }, onExpire: "archive" },
boosts: [{ weight: 0.25, startsAt: Date.now(), endsAt: Date.now() + 3600_000 }],
commentsEnabled: true
});
```
---
## β¨ Example: Subvault Usage
```js
// Add experience data under an item
const sv = v.addSubvault("item-123", "user-123", {
key: "stats",
payload: { level: 5, exp: 340 },
tags: ["progress", "gameplay"]
});
// Update later
v.updateSubvault(sv.id, { id: "user-123" }, { payload: { level: 6 } });
// Retrieve or upsert by key
v.upsertSubvaultByKey("item-123", "user-123", "stats", { level: 7 });
// List all Subvaults under an item
const list = v.listSubvault("item-123");
// Clone all Subvaults from one item to another branch
v.cloneSubvaultRecords("item-123", "item-999", { includeKeys: ["stats"] }, null, "subvault", "Experience_Stats");
```
---
## π License
See License in [LICENSE.md](./LICENSE.md)
---
> π **Ventry β The Gateway to User-Driven Content.**
> Transform anything into interactive, measurable, and living content with one API.