UNPKG

@hotmeshio/hotmesh

Version:

Permanent-Memory Workflows & AI Agents

387 lines (386 loc) 13.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PostgresSearchService = void 0; const index_1 = require("../../index"); const kvsql_1 = require("../../../store/providers/postgres/kvsql"); const key_1 = require("../../../../modules/key"); class PostgresSearchService extends index_1.SearchService { transact() { return this.storeClient.transact(); } constructor(searchClient, storeClient) { super(searchClient, storeClient); this.pgClient = searchClient; //raw pg client (to send raw sql) this.searchClient = new kvsql_1.KVSQL(//wrapped pg client searchClient, this.namespace, this.appId); } async init(namespace, appId, logger) { //bind appId and namespace to searchClient once initialized // (it uses these values to construct keys for the store) this.searchClient.namespace = this.namespace = namespace; this.searchClient.appId = this.appId = appId; this.namespace = namespace; this.appId = appId; this.logger = logger; } async createSearchIndex(indexName, prefixes, schema) { //no-op } async listSearchIndexes() { return []; } async updateContext(key, fields) { try { const result = await this.searchClient.hset(key, fields); return isNaN(result) ? result : Number(result); } catch (error) { this.logger.error(`postgres-search-set-fields-error`, { key, error }); throw error; } } async setFields(key, fields) { try { const result = await this.searchClient.hset(key, fields); const isGetOperation = '@context:get' in fields; if (isGetOperation) { return result; } return isNaN(result) ? result : Number(result); } catch (error) { this.logger.error(`postgres-search-set-fields-error`, { key, error }); throw error; } } async getField(key, field) { try { return await this.searchClient.hget(key, field); } catch (error) { this.logger.error(`postgres-search-get-field-error`, { key, field, error, }); throw error; } } async getFields(key, fields) { try { return await this.searchClient.hmget(key, [...fields]); } catch (error) { this.logger.error(`postgres-search-get-fields-error`, { key, fields, error, }); throw error; } } async getAllFields(key) { try { return await this.searchClient.hgetall(key); } catch (error) { this.logger.error(`postgres-search-get-all-fields-error`, { key, error, }); throw error; } } async deleteFields(key, fields) { try { const result = await this.searchClient.hdel(key, fields); return Number(result); } catch (error) { this.logger.error(`postgres-search-delete-fields-error`, { key, fields, error, }); throw error; } } async incrementFieldByFloat(key, field, increment) { try { const result = await this.searchClient.hincrbyfloat(key, field, increment); return Number(result); } catch (error) { this.logger.error(`postgres-increment-field-error`, { key, field, error, }); throw error; } } async sendQuery(query) { try { //exec raw sql (local call, not meant for external use); return raw result return await this.pgClient.query(query); } catch (error) { this.logger.error(`postgres-send-query-error`, { query, error }); throw error; } } /** * assume aggregation type query */ async sendIndexedQuery(type, queryParams = []) { const [sql, ...params] = queryParams; try { const res = await this.pgClient.query(sql, params.length ? params : undefined); return res.rows; } catch (error) { this.logger.error(`postgres-send-indexed-query-error`, { query: sql, error, }); throw error; } } // Entity querying methods for JSONB/SQL operations async findEntities(entity, conditions, options) { try { const schemaName = this.searchClient.safeName(this.appId); const tableName = `${schemaName}.${this.searchClient.safeName('jobs')}`; // Build WHERE conditions from the conditions object const whereConditions = [`entity = $1`]; const params = [entity]; let paramIndex = 2; for (const [key, value] of Object.entries(conditions)) { if (typeof value === 'object' && value !== null && !Array.isArray(value)) { // Handle MongoDB-style operators like { $gte: 18 } for (const [op, opValue] of Object.entries(value)) { const sqlOp = this.mongoToSqlOperator(op); whereConditions.push(`(context->>'${key}')::${this.inferType(opValue)} ${sqlOp} $${paramIndex}`); params.push(opValue); paramIndex++; } } else { // Simple equality whereConditions.push(`context->>'${key}' = $${paramIndex}`); params.push(String(value)); paramIndex++; } } let sql = ` SELECT key, context, status FROM ${tableName} WHERE ${whereConditions.join(' AND ')} ORDER BY created_at DESC `; if (options?.limit) { sql += ` LIMIT $${paramIndex}`; params.push(options.limit); paramIndex++; } if (options?.offset) { sql += ` OFFSET $${paramIndex}`; params.push(options.offset); } const result = await this.pgClient.query(sql, params); return result.rows.map((row) => ({ key: row.key, context: typeof row.context === 'string' ? JSON.parse(row.context || '{}') : row.context || {}, status: row.status, })); } catch (error) { this.logger.error(`postgres-find-entities-error`, { entity, conditions, error, }); throw error; } } async findEntityById(entity, id) { try { const schemaName = this.searchClient.safeName(this.appId); const tableName = `${schemaName}.${this.searchClient.safeName('jobs')}`; // Use KeyService to mint the job state key const fullKey = key_1.KeyService.mintKey(key_1.HMNS, key_1.KeyType.JOB_STATE, { appId: this.appId, jobId: id, }); const sql = ` SELECT key, context, status, entity FROM ${tableName} WHERE entity = $1 AND key = $2 LIMIT 1 `; const result = await this.pgClient.query(sql, [entity, fullKey]); if (result.rows.length === 0) { return null; } const row = result.rows[0]; return { key: row.key, context: typeof row.context === 'string' ? JSON.parse(row.context || '{}') : row.context || {}, status: row.status, }; } catch (error) { this.logger.error(`postgres-find-entity-by-id-error`, { entity, id, error, }); throw error; } } async findEntitiesByCondition(entity, field, value, operator = '=', options) { try { const schemaName = this.searchClient.safeName(this.appId); const tableName = `${schemaName}.${this.searchClient.safeName('jobs')}`; const params = [entity]; let whereCondition; let paramIndex = 2; if (operator === 'IN') { // Handle IN operator with arrays const placeholders = Array.isArray(value) ? value.map(() => `$${paramIndex++}`).join(',') : `$${paramIndex++}`; whereCondition = `context->>'${field}' IN (${placeholders})`; if (Array.isArray(value)) { params.push(...value); } else { params.push(value); } } else if (operator === 'LIKE') { whereCondition = `context->>'${field}' LIKE $${paramIndex}`; params.push(value); paramIndex++; } else { // Handle numeric/comparison operators const valueType = this.inferType(value); whereCondition = `(context->>'${field}')::${valueType} ${operator} $${paramIndex}`; params.push(value); paramIndex++; } let sql = ` SELECT key, context, status FROM ${tableName} WHERE entity = $1 AND ${whereCondition} ORDER BY created_at DESC `; if (options?.limit) { sql += ` LIMIT $${paramIndex}`; params.push(options.limit); paramIndex++; } if (options?.offset) { sql += ` OFFSET $${paramIndex}`; params.push(options.offset); } const result = await this.pgClient.query(sql, params); return result.rows.map((row) => ({ key: row.key, context: typeof row.context === 'string' ? JSON.parse(row.context || '{}') : row.context || {}, status: row.status, })); } catch (error) { this.logger.error(`postgres-find-entities-by-condition-error`, { entity, field, value, operator, error, }); throw error; } } async createEntityIndex(entity, field, indexType = 'btree') { try { const schemaName = this.searchClient.safeName(this.appId); const tableName = `${schemaName}.${this.searchClient.safeName('jobs')}`; const indexName = `idx_${this.appId}_${entity}_${field}`.replace(/[^a-zA-Z0-9_]/g, '_'); let sql; if (indexType === 'gin') { // GIN index for JSONB operations sql = ` CREATE INDEX IF NOT EXISTS ${indexName} ON ${tableName} USING gin (context jsonb_path_ops) WHERE entity = '${entity}' `; } else if (indexType === 'gist') { // GiST index for specific field sql = ` CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE INDEX IF NOT EXISTS ${indexName} ON ${tableName} USING gist ((context->>'${field}') gist_trgm_ops) WHERE entity = '${entity}' `; } else { // B-tree index for specific field sql = ` CREATE INDEX IF NOT EXISTS ${indexName} ON ${tableName} USING btree ((context->>'${field}')) WHERE entity = '${entity}' `; } await this.pgClient.query(sql); this.logger.info(`postgres-entity-index-created`, { entity, field, indexType, indexName, }); } catch (error) { this.logger.error(`postgres-create-entity-index-error`, { entity, field, indexType, error, }); throw error; } } // Helper methods for entity operations mongoToSqlOperator(mongoOp) { const mapping = { $eq: '=', $ne: '!=', $gt: '>', $gte: '>=', $lt: '<', $lte: '<=', $in: 'IN', }; return mapping[mongoOp] || '='; } inferType(value) { if (typeof value === 'number') { return Number.isInteger(value) ? 'integer' : 'numeric'; } if (typeof value === 'boolean') { return 'boolean'; } return 'text'; } } exports.PostgresSearchService = PostgresSearchService;