UNPKG

@hotmeshio/hotmesh

Version:

Permanent-Memory Workflows & AI Agents

462 lines (461 loc) 16.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Entity = void 0; const key_1 = require("../../modules/key"); const storage_1 = require("../../modules/storage"); /** * The Entity module provides methods for reading and writing * JSONB data to a workflow's entity. The instance methods * exposed by this class are available for use from within * a running workflow. * * @example * ```typescript * //entityWorkflow.ts * import { workflow } from '@hotmeshio/hotmesh'; * * export async function entityExample(): Promise<void> { * const entity = await workflow.entity(); * await entity.set({ user: { id: 123 } }); * await entity.merge({ user: { name: "John" } }); * const user = await entity.get("user"); * // user = { id: 123, name: "John" } * } * ``` */ class Entity { /** * @private */ constructor(workflowId, hotMeshClient, searchSessionId) { /** * @private */ this.searchSessionIndex = 0; const keyParams = { appId: hotMeshClient.appId, jobId: workflowId, }; this.jobId = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams); this.searchSessionId = searchSessionId; this.hotMeshClient = hotMeshClient; this.search = hotMeshClient.engine.search; // Get workflow dimension from async local storage const store = storage_1.asyncLocalStorage.getStore(); this.workflowDimension = store?.get('workflowDimension') ?? ''; } /** * increments the index to return a unique search session guid when * calling any method that produces side effects (changes the value) * @private */ getSearchSessionGuid() { return `${this.searchSessionId}-${this.searchSessionIndex++}-`; } /** * Sets the entire entity object. This replaces any existing entity. * * @example * const entity = await workflow.entity(); * await entity.set({ user: { id: 123, name: "John" } }); */ async set(value) { const ssGuid = this.getSearchSessionGuid(); const store = storage_1.asyncLocalStorage.getStore(); const replay = store?.get('replay') ?? {}; if (ssGuid in replay) { return JSON.parse(replay[ssGuid]); } // Use single transactional call to update entity and store replay value const result = await this.search.updateContext(this.jobId, { '@context': JSON.stringify(value), [ssGuid]: '', // Pass replay ID to hash module for transactional replay storage }); return result; } /** * Deep merges the provided object with the existing entity * * @example * const entity = await workflow.entity(); * await entity.merge({ user: { email: "john@example.com" } }); */ async merge(value) { const ssGuid = this.getSearchSessionGuid(); const store = storage_1.asyncLocalStorage.getStore(); const replay = store?.get('replay') ?? {}; if (ssGuid in replay) { return JSON.parse(replay[ssGuid]); } // Use server-side JSONB merge operation with replay storage const newContext = await this.search.updateContext(this.jobId, { '@context:merge': JSON.stringify(value), [ssGuid]: '', // Pass replay ID to hash module }); return newContext; } /** * Gets a value from the entity by path * * @example * const entity = await workflow.entity(); * const user = await entity.get("user"); * const email = await entity.get("user.email"); */ async get(path) { const ssGuid = this.getSearchSessionGuid(); const store = storage_1.asyncLocalStorage.getStore(); const replay = store?.get('replay') ?? {}; if (ssGuid in replay) { // Replay cache stores the already-extracted value, not full entity return JSON.parse(replay[ssGuid]); } let value; if (!path) { // No path - fetch entire entity with replay storage const result = await this.search.updateContext(this.jobId, { '@context:get': '', [ssGuid]: '', // Pass replay ID to hash module }); // setFields returns the actual entity value for @context:get operations value = result || {}; } else { // Use PostgreSQL JSONB path extraction for specific paths with replay storage const result = await this.search.updateContext(this.jobId, { '@context:get': path, [ssGuid]: '', // Pass replay ID to hash module }); // setFields returns the actual path value for @context:get operations value = result; } return value; } /** * Deletes a value from the entity by path * * @example * const entity = await workflow.entity(); * await entity.delete("user.email"); */ async delete(path) { const ssGuid = this.getSearchSessionGuid(); const store = storage_1.asyncLocalStorage.getStore(); const replay = store?.get('replay') ?? {}; if (ssGuid in replay) { return JSON.parse(replay[ssGuid]); } // Use server-side JSONB delete operation with replay storage const newContext = await this.search.updateContext(this.jobId, { '@context:delete': path, [ssGuid]: '', // Pass replay ID to hash module }); return newContext; } /** * Appends a value to an array at the specified path * * @example * const entity = await workflow.entity(); * await entity.append("items", { id: 1, name: "New Item" }); */ async append(path, value) { const ssGuid = this.getSearchSessionGuid(); const store = storage_1.asyncLocalStorage.getStore(); const replay = store?.get('replay') ?? {}; if (ssGuid in replay) { return JSON.parse(replay[ssGuid]); } // Use server-side JSONB array append operation with replay storage const newArray = await this.search.updateContext(this.jobId, { '@context:append': JSON.stringify({ path, value }), [ssGuid]: '', // Pass replay ID to hash module }); return newArray; } /** * Prepends a value to an array at the specified path * * @example * const entity = await workflow.entity(); * await entity.prepend("items", { id: 0, name: "First Item" }); */ async prepend(path, value) { const ssGuid = this.getSearchSessionGuid(); const store = storage_1.asyncLocalStorage.getStore(); const replay = store?.get('replay') ?? {}; if (ssGuid in replay) { return JSON.parse(replay[ssGuid]); } // Use server-side JSONB array prepend operation with replay storage const newArray = await this.search.updateContext(this.jobId, { '@context:prepend': JSON.stringify({ path, value }), [ssGuid]: '', // Pass replay ID to hash module }); return newArray; } /** * Removes an item from an array at the specified path and index * * @example * const entity = await workflow.entity(); * await entity.remove("items", 0); // Remove first item */ async remove(path, index) { const ssGuid = this.getSearchSessionGuid(); const store = storage_1.asyncLocalStorage.getStore(); const replay = store?.get('replay') ?? {}; if (ssGuid in replay) { return JSON.parse(replay[ssGuid]); } // Use server-side JSONB array remove operation with replay storage const newArray = await this.search.updateContext(this.jobId, { '@context:remove': JSON.stringify({ path, index }), [ssGuid]: '', // Pass replay ID to hash module }); return newArray; } /** * Increments a numeric value at the specified path * * @example * const entity = await workflow.entity(); * await entity.increment("counter", 5); */ async increment(path, value = 1) { const ssGuid = this.getSearchSessionGuid(); const store = storage_1.asyncLocalStorage.getStore(); const replay = store?.get('replay') ?? {}; if (ssGuid in replay) { return JSON.parse(replay[ssGuid]); } // Use server-side JSONB increment operation with replay storage const newValue = await this.search.updateContext(this.jobId, { '@context:increment': JSON.stringify({ path, value }), [ssGuid]: '', // Pass replay ID to hash module }); return Number(newValue); } /** * Toggles a boolean value at the specified path * * @example * const entity = await workflow.entity(); * await entity.toggle("settings.enabled"); */ async toggle(path) { const ssGuid = this.getSearchSessionGuid(); const store = storage_1.asyncLocalStorage.getStore(); const replay = store?.get('replay') ?? {}; if (ssGuid in replay) { return JSON.parse(replay[ssGuid]); } // Use server-side JSONB toggle operation with replay storage const newValue = await this.search.updateContext(this.jobId, { '@context:toggle': path, [ssGuid]: '', // Pass replay ID to hash module }); return Boolean(newValue); } /** * Sets a value at the specified path only if it doesn't already exist * * @example * const entity = await workflow.entity(); * await entity.setIfNotExists("user.id", 123); */ async setIfNotExists(path, value) { const ssGuid = this.getSearchSessionGuid(); const store = storage_1.asyncLocalStorage.getStore(); const replay = store?.get('replay') ?? {}; if (ssGuid in replay) { return JSON.parse(replay[ssGuid]); } // Use server-side JSONB conditional set operation with replay storage const newValue = await this.search.updateContext(this.jobId, { '@context:setIfNotExists': JSON.stringify({ path, value }), [ssGuid]: '', // Pass replay ID to hash module }); return newValue; } // Static readonly find methods for cross-entity querying (not tied to specific workflow) /** * Finds entity records matching complex conditions using JSONB/SQL queries. * This is a readonly operation that queries across all entities of a given type. * * @example * ```typescript * // Basic find with simple conditions * const activeUsers = await Entity.find( * 'user', * { status: 'active', country: 'US' }, * hotMeshClient * ); * * // Complex query with comparison operators * const seniorUsers = await Entity.find( * 'user', * { * age: { $gte: 65 }, * status: 'active', * 'preferences.notifications': true * }, * hotMeshClient, * { limit: 10, offset: 0 } * ); * * // Query with multiple conditions and nested objects * const premiumUsers = await Entity.find( * 'user', * { * 'subscription.type': 'premium', * 'subscription.status': 'active', * 'billing.amount': { $gt: 100 }, * 'profile.verified': true * }, * hotMeshClient, * { limit: 20 } * ); * * // Array conditions * const taggedPosts = await Entity.find( * 'post', * { * 'tags': { $in: ['typescript', 'javascript'] }, * 'status': 'published', * 'views': { $gte: 1000 } * }, * hotMeshClient * ); * ``` */ static async find(entity, conditions, hotMeshClient, options) { // Use SearchService for JSONB/SQL querying const searchClient = hotMeshClient.engine.search; return await searchClient.findEntities(entity, conditions, options); } /** * Finds a specific entity record by its ID using direct JSONB/SQL queries. * This is the most efficient method for retrieving a single entity record. * * @example * ```typescript * // Basic findById usage * const user = await Entity.findById('user', 'user123', hotMeshClient); * * // Example with type checking * interface User { * id: string; * name: string; * email: string; * preferences: { * theme: 'light' | 'dark'; * notifications: boolean; * }; * } * * const typedUser = await Entity.findById<User>('user', 'user456', hotMeshClient); * console.log(typedUser.preferences.theme); // 'light' | 'dark' * * // Error handling example * try { * const order = await Entity.findById('order', 'order789', hotMeshClient); * if (!order) { * console.log('Order not found'); * return; * } * console.log('Order details:', order); * } catch (error) { * console.error('Error fetching order:', error); * } * ``` */ static async findById(entity, id, hotMeshClient) { // Use SearchService for JSONB/SQL querying const searchClient = hotMeshClient.engine.search; return await searchClient.findEntityById(entity, id); } /** * Finds entity records matching a specific field condition using JSONB/SQL queries. * Supports various operators for flexible querying across all entities of a type. * * @example * ```typescript * // Basic equality search * const activeUsers = await Entity.findByCondition( * 'user', * 'status', * 'active', * '=', * hotMeshClient, * { limit: 20 } * ); * * // Numeric comparison * const highValueOrders = await Entity.findByCondition( * 'order', * 'total_amount', * 1000, * '>=', * hotMeshClient * ); * * // Pattern matching with LIKE * const gmailUsers = await Entity.findByCondition( * 'user', * 'email', * '%@gmail.com', * 'LIKE', * hotMeshClient * ); * * // IN operator for multiple values * const specificProducts = await Entity.findByCondition( * 'product', * 'category', * ['electronics', 'accessories'], * 'IN', * hotMeshClient * ); * * // Not equals operator * const nonPremiumUsers = await Entity.findByCondition( * 'user', * 'subscription_type', * 'premium', * '!=', * hotMeshClient * ); * * // Date comparison * const recentOrders = await Entity.findByCondition( * 'order', * 'created_at', * new Date('2024-01-01'), * '>', * hotMeshClient, * { limit: 50 } * ); * ``` */ static async findByCondition(entity, field, value, operator = '=', hotMeshClient, options) { // Use SearchService for JSONB/SQL querying const searchClient = hotMeshClient.engine.search; return await searchClient.findEntitiesByCondition(entity, field, value, operator, options); } /** * Creates an efficient GIN index for a specific entity field to optimize queries. * * @example * ```typescript * await Entity.createIndex('user', 'email', hotMeshClient); * await Entity.createIndex('user', 'status', hotMeshClient); * ``` */ static async createIndex(entity, field, hotMeshClient, indexType = 'gin') { // Use SearchService for index creation const searchClient = hotMeshClient.engine.search; return await searchClient.createEntityIndex(entity, field, indexType); } } exports.Entity = Entity;