UNPKG

@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
# 🌐 @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.