@hotmeshio/hotmesh
Version:
Permanent-Memory Workflows & AI Agents
387 lines (386 loc) • 13.3 kB
JavaScript
;
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;