@hotmeshio/hotmesh 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +37 -48
  2. package/build/package.json +16 -14
  3. package/build/services/hotmesh/index.d.ts +9 -11
  4. package/build/services/hotmesh/index.js +9 -11
  5. package/build/services/memflow/entity.d.ts +168 -4
  6. package/build/services/memflow/entity.js +177 -15
  7. package/build/services/memflow/workflow/index.d.ts +2 -4
  8. package/build/services/memflow/workflow/index.js +2 -4
  9. package/build/services/memflow/workflow/interruption.d.ts +6 -4
  10. package/build/services/memflow/workflow/interruption.js +6 -4
  11. package/build/services/memflow/workflow/waitFor.js +1 -0
  12. package/build/services/search/index.d.ts +10 -0
  13. package/build/services/search/providers/postgres/postgres.d.ts +12 -0
  14. package/build/services/search/providers/postgres/postgres.js +209 -0
  15. package/build/services/search/providers/redis/ioredis.d.ts +4 -0
  16. package/build/services/search/providers/redis/ioredis.js +13 -0
  17. package/build/services/search/providers/redis/redis.d.ts +4 -0
  18. package/build/services/search/providers/redis/redis.js +13 -0
  19. package/build/services/store/providers/postgres/kvsql.d.ts +13 -37
  20. package/build/services/store/providers/postgres/kvsql.js +2 -2
  21. package/build/services/store/providers/postgres/kvtypes/hash/basic.d.ts +16 -0
  22. package/build/services/store/providers/postgres/kvtypes/hash/basic.js +480 -0
  23. package/build/services/store/providers/postgres/kvtypes/hash/expire.d.ts +5 -0
  24. package/build/services/store/providers/postgres/kvtypes/hash/expire.js +33 -0
  25. package/build/services/store/providers/postgres/kvtypes/hash/index.d.ts +29 -0
  26. package/build/services/store/providers/postgres/kvtypes/hash/index.js +190 -0
  27. package/build/services/store/providers/postgres/kvtypes/hash/jsonb.d.ts +14 -0
  28. package/build/services/store/providers/postgres/kvtypes/hash/jsonb.js +699 -0
  29. package/build/services/store/providers/postgres/kvtypes/hash/scan.d.ts +10 -0
  30. package/build/services/store/providers/postgres/kvtypes/hash/scan.js +91 -0
  31. package/build/services/store/providers/postgres/kvtypes/hash/types.d.ts +19 -0
  32. package/build/services/store/providers/postgres/kvtypes/hash/types.js +2 -0
  33. package/build/services/store/providers/postgres/kvtypes/hash/utils.d.ts +18 -0
  34. package/build/services/store/providers/postgres/kvtypes/hash/utils.js +90 -0
  35. package/build/types/memflow.d.ts +1 -1
  36. package/build/types/meshdata.d.ts +1 -1
  37. package/package.json +16 -14
  38. package/build/services/store/providers/postgres/kvtypes/hash.d.ts +0 -60
  39. package/build/services/store/providers/postgres/kvtypes/hash.js +0 -1287
@@ -71,7 +71,7 @@ class Entity {
71
71
  '@context': JSON.stringify(value),
72
72
  [ssGuid]: '', // Pass replay ID to hash module for transactional replay storage
73
73
  });
74
- return result || value;
74
+ return result;
75
75
  }
76
76
  /**
77
77
  * Deep merges the provided object with the existing entity
@@ -278,22 +278,184 @@ class Entity {
278
278
  });
279
279
  return newValue;
280
280
  }
281
+ // Static readonly find methods for cross-entity querying (not tied to specific workflow)
281
282
  /**
282
- * @private
283
+ * Finds entity records matching complex conditions using JSONB/SQL queries.
284
+ * This is a readonly operation that queries across all entities of a given type.
285
+ *
286
+ * @example
287
+ * ```typescript
288
+ * // Basic find with simple conditions
289
+ * const activeUsers = await Entity.find(
290
+ * 'user',
291
+ * { status: 'active', country: 'US' },
292
+ * hotMeshClient
293
+ * );
294
+ *
295
+ * // Complex query with comparison operators
296
+ * const seniorUsers = await Entity.find(
297
+ * 'user',
298
+ * {
299
+ * age: { $gte: 65 },
300
+ * status: 'active',
301
+ * 'preferences.notifications': true
302
+ * },
303
+ * hotMeshClient,
304
+ * { limit: 10, offset: 0 }
305
+ * );
306
+ *
307
+ * // Query with multiple conditions and nested objects
308
+ * const premiumUsers = await Entity.find(
309
+ * 'user',
310
+ * {
311
+ * 'subscription.type': 'premium',
312
+ * 'subscription.status': 'active',
313
+ * 'billing.amount': { $gt: 100 },
314
+ * 'profile.verified': true
315
+ * },
316
+ * hotMeshClient,
317
+ * { limit: 20 }
318
+ * );
319
+ *
320
+ * // Array conditions
321
+ * const taggedPosts = await Entity.find(
322
+ * 'post',
323
+ * {
324
+ * 'tags': { $in: ['typescript', 'javascript'] },
325
+ * 'status': 'published',
326
+ * 'views': { $gte: 1000 }
327
+ * },
328
+ * hotMeshClient
329
+ * );
330
+ * ```
283
331
  */
284
- deepMerge(target, source) {
285
- if (!source)
286
- return target;
287
- const output = { ...target };
288
- Object.keys(source).forEach(key => {
289
- if (source[key] instanceof Object && key in target) {
290
- output[key] = this.deepMerge(target[key], source[key]);
291
- }
292
- else {
293
- output[key] = source[key];
294
- }
295
- });
296
- return output;
332
+ static async find(entity, conditions, hotMeshClient, options) {
333
+ // Use SearchService for JSONB/SQL querying
334
+ const searchClient = hotMeshClient.engine.search;
335
+ return await searchClient.findEntities(entity, conditions, options);
336
+ }
337
+ /**
338
+ * Finds a specific entity record by its ID using direct JSONB/SQL queries.
339
+ * This is the most efficient method for retrieving a single entity record.
340
+ *
341
+ * @example
342
+ * ```typescript
343
+ * // Basic findById usage
344
+ * const user = await Entity.findById('user', 'user123', hotMeshClient);
345
+ *
346
+ * // Example with type checking
347
+ * interface User {
348
+ * id: string;
349
+ * name: string;
350
+ * email: string;
351
+ * preferences: {
352
+ * theme: 'light' | 'dark';
353
+ * notifications: boolean;
354
+ * };
355
+ * }
356
+ *
357
+ * const typedUser = await Entity.findById<User>('user', 'user456', hotMeshClient);
358
+ * console.log(typedUser.preferences.theme); // 'light' | 'dark'
359
+ *
360
+ * // Error handling example
361
+ * try {
362
+ * const order = await Entity.findById('order', 'order789', hotMeshClient);
363
+ * if (!order) {
364
+ * console.log('Order not found');
365
+ * return;
366
+ * }
367
+ * console.log('Order details:', order);
368
+ * } catch (error) {
369
+ * console.error('Error fetching order:', error);
370
+ * }
371
+ * ```
372
+ */
373
+ static async findById(entity, id, hotMeshClient) {
374
+ // Use SearchService for JSONB/SQL querying
375
+ const searchClient = hotMeshClient.engine.search;
376
+ return await searchClient.findEntityById(entity, id);
377
+ }
378
+ /**
379
+ * Finds entity records matching a specific field condition using JSONB/SQL queries.
380
+ * Supports various operators for flexible querying across all entities of a type.
381
+ *
382
+ * @example
383
+ * ```typescript
384
+ * // Basic equality search
385
+ * const activeUsers = await Entity.findByCondition(
386
+ * 'user',
387
+ * 'status',
388
+ * 'active',
389
+ * '=',
390
+ * hotMeshClient,
391
+ * { limit: 20 }
392
+ * );
393
+ *
394
+ * // Numeric comparison
395
+ * const highValueOrders = await Entity.findByCondition(
396
+ * 'order',
397
+ * 'total_amount',
398
+ * 1000,
399
+ * '>=',
400
+ * hotMeshClient
401
+ * );
402
+ *
403
+ * // Pattern matching with LIKE
404
+ * const gmailUsers = await Entity.findByCondition(
405
+ * 'user',
406
+ * 'email',
407
+ * '%@gmail.com',
408
+ * 'LIKE',
409
+ * hotMeshClient
410
+ * );
411
+ *
412
+ * // IN operator for multiple values
413
+ * const specificProducts = await Entity.findByCondition(
414
+ * 'product',
415
+ * 'category',
416
+ * ['electronics', 'accessories'],
417
+ * 'IN',
418
+ * hotMeshClient
419
+ * );
420
+ *
421
+ * // Not equals operator
422
+ * const nonPremiumUsers = await Entity.findByCondition(
423
+ * 'user',
424
+ * 'subscription_type',
425
+ * 'premium',
426
+ * '!=',
427
+ * hotMeshClient
428
+ * );
429
+ *
430
+ * // Date comparison
431
+ * const recentOrders = await Entity.findByCondition(
432
+ * 'order',
433
+ * 'created_at',
434
+ * new Date('2024-01-01'),
435
+ * '>',
436
+ * hotMeshClient,
437
+ * { limit: 50 }
438
+ * );
439
+ * ```
440
+ */
441
+ static async findByCondition(entity, field, value, operator = '=', hotMeshClient, options) {
442
+ // Use SearchService for JSONB/SQL querying
443
+ const searchClient = hotMeshClient.engine.search;
444
+ return await searchClient.findEntitiesByCondition(entity, field, value, operator, options);
445
+ }
446
+ /**
447
+ * Creates an efficient GIN index for a specific entity field to optimize queries.
448
+ *
449
+ * @example
450
+ * ```typescript
451
+ * await Entity.createIndex('user', 'email', hotMeshClient);
452
+ * await Entity.createIndex('user', 'status', hotMeshClient);
453
+ * ```
454
+ */
455
+ static async createIndex(entity, field, hotMeshClient, indexType = 'gin') {
456
+ // Use SearchService for index creation
457
+ const searchClient = hotMeshClient.engine.search;
458
+ return await searchClient.createEntityIndex(entity, field, indexType);
297
459
  }
298
460
  }
299
461
  exports.Entity = Entity;
@@ -12,6 +12,7 @@ import { random } from './random';
12
12
  import { signal } from './signal';
13
13
  import { hook } from './hook';
14
14
  import { interrupt } from './interrupt';
15
+ import { didInterrupt } from './interruption';
15
16
  import { all } from './all';
16
17
  import { sleepFor } from './sleepFor';
17
18
  import { waitFor } from './waitFor';
@@ -22,10 +23,6 @@ import { entity } from './entityMethods';
22
23
  * These methods ensure deterministic replay, persistence of state, and error handling across
23
24
  * re-entrant workflow executions.
24
25
  *
25
- * By refactoring the original single-file implementation into submodules,
26
- * we maintain clear separation of concerns and improved maintainability,
27
- * while preserving type information and full functionality.
28
- *
29
26
  * @example
30
27
  * ```typescript
31
28
  * import { MemFlow } from '@hotmeshio/hotmesh';
@@ -62,6 +59,7 @@ export declare class WorkflowService {
62
59
  static random: typeof random;
63
60
  static signal: typeof signal;
64
61
  static hook: typeof hook;
62
+ static didInterrupt: typeof didInterrupt;
65
63
  static interrupt: typeof interrupt;
66
64
  static all: typeof all;
67
65
  static sleepFor: typeof sleepFor;
@@ -15,6 +15,7 @@ const random_1 = require("./random");
15
15
  const signal_1 = require("./signal");
16
16
  const hook_1 = require("./hook");
17
17
  const interrupt_1 = require("./interrupt");
18
+ const interruption_1 = require("./interruption");
18
19
  const all_1 = require("./all");
19
20
  const sleepFor_1 = require("./sleepFor");
20
21
  const waitFor_1 = require("./waitFor");
@@ -25,10 +26,6 @@ const entityMethods_1 = require("./entityMethods");
25
26
  * These methods ensure deterministic replay, persistence of state, and error handling across
26
27
  * re-entrant workflow executions.
27
28
  *
28
- * By refactoring the original single-file implementation into submodules,
29
- * we maintain clear separation of concerns and improved maintainability,
30
- * while preserving type information and full functionality.
31
- *
32
29
  * @example
33
30
  * ```typescript
34
31
  * import { MemFlow } from '@hotmeshio/hotmesh';
@@ -80,6 +77,7 @@ WorkflowService.entity = entityMethods_1.entity;
80
77
  WorkflowService.random = random_1.random;
81
78
  WorkflowService.signal = signal_1.signal;
82
79
  WorkflowService.hook = hook_1.hook;
80
+ WorkflowService.didInterrupt = interruption_1.didInterrupt;
83
81
  WorkflowService.interrupt = interrupt_1.interrupt;
84
82
  WorkflowService.all = all_1.all;
85
83
  WorkflowService.sleepFor = sleepFor_1.sleepFor;
@@ -1,18 +1,20 @@
1
1
  /**
2
2
  * Checks if an error is a HotMesh reserved error type that indicates
3
- * a workflow interruption rather than a true error condition.
3
+ * a HotMesh interruption rather than a true error condition.
4
4
  *
5
- * When this returns true, you can safely return from your workflow function.
5
+ * When this returns true, you can safely return rethrow the error.
6
6
  * The workflow engine will handle the interruption automatically.
7
7
  *
8
8
  * @example
9
9
  * ```typescript
10
+ * import { MemFlow } from '@hotmeshio/hotmesh';
11
+ *
10
12
  * try {
11
13
  * await someWorkflowOperation();
12
14
  * } catch (error) {
13
15
  * // Check if this is a HotMesh interruption
14
- * if (didInterrupt(error)) {
15
- * // Rethrow the error if HotMesh interruption
16
+ * if (MemFlow.workflow.didInterrupt(error)) {
17
+ * // Rethrow the error
16
18
  * throw error;
17
19
  * }
18
20
  * // Handle actual error
@@ -4,19 +4,21 @@ exports.didInterrupt = void 0;
4
4
  const errors_1 = require("../../../modules/errors");
5
5
  /**
6
6
  * Checks if an error is a HotMesh reserved error type that indicates
7
- * a workflow interruption rather than a true error condition.
7
+ * a HotMesh interruption rather than a true error condition.
8
8
  *
9
- * When this returns true, you can safely return from your workflow function.
9
+ * When this returns true, you can safely return rethrow the error.
10
10
  * The workflow engine will handle the interruption automatically.
11
11
  *
12
12
  * @example
13
13
  * ```typescript
14
+ * import { MemFlow } from '@hotmeshio/hotmesh';
15
+ *
14
16
  * try {
15
17
  * await someWorkflowOperation();
16
18
  * } catch (error) {
17
19
  * // Check if this is a HotMesh interruption
18
- * if (didInterrupt(error)) {
19
- * // Rethrow the error if HotMesh interruption
20
+ * if (MemFlow.workflow.didInterrupt(error)) {
21
+ * // Rethrow the error
20
22
  * throw error;
21
23
  * }
22
24
  * // Handle actual error
@@ -50,6 +50,7 @@ async function waitFor(signalId) {
50
50
  };
51
51
  interruptionRegistry.push(interruptionMessage);
52
52
  await (0, common_1.sleepImmediate)();
53
+ //if you are seeing this error in the logs, you might have forgotten to `await waitFor(...)`
53
54
  throw new common_1.MemFlowWaitForError(interruptionMessage);
54
55
  }
55
56
  exports.waitFor = waitFor;
@@ -19,5 +19,15 @@ declare abstract class SearchService<ClientProvider extends ProviderClient> {
19
19
  abstract incrementFieldByFloat(key: string, field: string, increment: number): Promise<number>;
20
20
  abstract sendQuery(query: any): Promise<any>;
21
21
  abstract sendIndexedQuery(index: string, query: any[]): Promise<any>;
22
+ abstract findEntities(entity: string, conditions: Record<string, any>, options?: {
23
+ limit?: number;
24
+ offset?: number;
25
+ }): Promise<any[]>;
26
+ abstract findEntityById(entity: string, id: string): Promise<any>;
27
+ abstract findEntitiesByCondition(entity: string, field: string, value: any, operator?: '=' | '!=' | '>' | '<' | '>=' | '<=' | 'LIKE' | 'IN', options?: {
28
+ limit?: number;
29
+ offset?: number;
30
+ }): Promise<any[]>;
31
+ abstract createEntityIndex(entity: string, field: string, indexType?: 'btree' | 'gin' | 'gist'): Promise<void>;
22
32
  }
23
33
  export { SearchService };
@@ -21,5 +21,17 @@ declare class PostgresSearchService extends SearchService<PostgresClientType & P
21
21
  * assume aggregation type query
22
22
  */
23
23
  sendIndexedQuery(type: string, queryParams?: any[]): Promise<any[]>;
24
+ findEntities(entity: string, conditions: Record<string, any>, options?: {
25
+ limit?: number;
26
+ offset?: number;
27
+ }): Promise<any[]>;
28
+ findEntityById(entity: string, id: string): Promise<any>;
29
+ findEntitiesByCondition(entity: string, field: string, value: any, operator?: '=' | '!=' | '>' | '<' | '>=' | '<=' | 'LIKE' | 'IN', options?: {
30
+ limit?: number;
31
+ offset?: number;
32
+ }): Promise<any[]>;
33
+ createEntityIndex(entity: string, field: string, indexType?: 'btree' | 'gin' | 'gist'): Promise<void>;
34
+ private mongoToSqlOperator;
35
+ private inferType;
24
36
  }
25
37
  export { PostgresSearchService };
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PostgresSearchService = void 0;
4
4
  const index_1 = require("../../index");
5
5
  const kvsql_1 = require("../../../store/providers/postgres/kvsql");
6
+ const key_1 = require("../../../../modules/key");
6
7
  class PostgresSearchService extends index_1.SearchService {
7
8
  transact() {
8
9
  return this.storeClient.transact();
@@ -145,5 +146,213 @@ class PostgresSearchService extends index_1.SearchService {
145
146
  throw error;
146
147
  }
147
148
  }
149
+ // Entity querying methods for JSONB/SQL operations
150
+ async findEntities(entity, conditions, options) {
151
+ try {
152
+ const schemaName = this.searchClient.safeName(this.appId);
153
+ const tableName = `${schemaName}.${this.searchClient.safeName('jobs')}`;
154
+ // Build WHERE conditions from the conditions object
155
+ const whereConditions = [`entity = $1`];
156
+ const params = [entity];
157
+ let paramIndex = 2;
158
+ for (const [key, value] of Object.entries(conditions)) {
159
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
160
+ // Handle MongoDB-style operators like { $gte: 18 }
161
+ for (const [op, opValue] of Object.entries(value)) {
162
+ const sqlOp = this.mongoToSqlOperator(op);
163
+ whereConditions.push(`(context->>'${key}')::${this.inferType(opValue)} ${sqlOp} $${paramIndex}`);
164
+ params.push(opValue);
165
+ paramIndex++;
166
+ }
167
+ }
168
+ else {
169
+ // Simple equality
170
+ whereConditions.push(`context->>'${key}' = $${paramIndex}`);
171
+ params.push(String(value));
172
+ paramIndex++;
173
+ }
174
+ }
175
+ let sql = `
176
+ SELECT key, context, status
177
+ FROM ${tableName}
178
+ WHERE ${whereConditions.join(' AND ')}
179
+ ORDER BY created_at DESC
180
+ `;
181
+ if (options?.limit) {
182
+ sql += ` LIMIT $${paramIndex}`;
183
+ params.push(options.limit);
184
+ paramIndex++;
185
+ }
186
+ if (options?.offset) {
187
+ sql += ` OFFSET $${paramIndex}`;
188
+ params.push(options.offset);
189
+ }
190
+ const result = await this.pgClient.query(sql, params);
191
+ return result.rows.map(row => ({
192
+ key: row.key,
193
+ context: typeof row.context === 'string' ? JSON.parse(row.context || '{}') : (row.context || {}),
194
+ status: row.status,
195
+ }));
196
+ }
197
+ catch (error) {
198
+ this.logger.error(`postgres-find-entities-error`, { entity, conditions, error });
199
+ throw error;
200
+ }
201
+ }
202
+ async findEntityById(entity, id) {
203
+ try {
204
+ const schemaName = this.searchClient.safeName(this.appId);
205
+ const tableName = `${schemaName}.${this.searchClient.safeName('jobs')}`;
206
+ // Use KeyService to mint the job state key
207
+ const fullKey = key_1.KeyService.mintKey(key_1.HMNS, key_1.KeyType.JOB_STATE, {
208
+ appId: this.appId,
209
+ jobId: id
210
+ });
211
+ const sql = `
212
+ SELECT key, context, status, entity
213
+ FROM ${tableName}
214
+ WHERE entity = $1 AND key = $2
215
+ LIMIT 1
216
+ `;
217
+ const result = await this.pgClient.query(sql, [entity, fullKey]);
218
+ if (result.rows.length === 0) {
219
+ return null;
220
+ }
221
+ const row = result.rows[0];
222
+ return {
223
+ key: row.key,
224
+ context: typeof row.context === 'string' ? JSON.parse(row.context || '{}') : (row.context || {}),
225
+ status: row.status,
226
+ };
227
+ }
228
+ catch (error) {
229
+ this.logger.error(`postgres-find-entity-by-id-error`, { entity, id, error });
230
+ throw error;
231
+ }
232
+ }
233
+ async findEntitiesByCondition(entity, field, value, operator = '=', options) {
234
+ try {
235
+ const schemaName = this.searchClient.safeName(this.appId);
236
+ const tableName = `${schemaName}.${this.searchClient.safeName('jobs')}`;
237
+ const params = [entity];
238
+ let whereCondition;
239
+ let paramIndex = 2;
240
+ if (operator === 'IN') {
241
+ // Handle IN operator with arrays
242
+ const placeholders = Array.isArray(value)
243
+ ? value.map(() => `$${paramIndex++}`).join(',')
244
+ : `$${paramIndex++}`;
245
+ whereCondition = `context->>'${field}' IN (${placeholders})`;
246
+ if (Array.isArray(value)) {
247
+ params.push(...value);
248
+ }
249
+ else {
250
+ params.push(value);
251
+ }
252
+ }
253
+ else if (operator === 'LIKE') {
254
+ whereCondition = `context->>'${field}' LIKE $${paramIndex}`;
255
+ params.push(value);
256
+ paramIndex++;
257
+ }
258
+ else {
259
+ // Handle numeric/comparison operators
260
+ const valueType = this.inferType(value);
261
+ whereCondition = `(context->>'${field}')::${valueType} ${operator} $${paramIndex}`;
262
+ params.push(value);
263
+ paramIndex++;
264
+ }
265
+ let sql = `
266
+ SELECT key, context, status
267
+ FROM ${tableName}
268
+ WHERE entity = $1 AND ${whereCondition}
269
+ ORDER BY created_at DESC
270
+ `;
271
+ if (options?.limit) {
272
+ sql += ` LIMIT $${paramIndex}`;
273
+ params.push(options.limit);
274
+ paramIndex++;
275
+ }
276
+ if (options?.offset) {
277
+ sql += ` OFFSET $${paramIndex}`;
278
+ params.push(options.offset);
279
+ }
280
+ const result = await this.pgClient.query(sql, params);
281
+ return result.rows.map(row => ({
282
+ key: row.key,
283
+ context: typeof row.context === 'string' ? JSON.parse(row.context || '{}') : (row.context || {}),
284
+ status: row.status,
285
+ }));
286
+ }
287
+ catch (error) {
288
+ this.logger.error(`postgres-find-entities-by-condition-error`, {
289
+ entity, field, value, operator, error
290
+ });
291
+ throw error;
292
+ }
293
+ }
294
+ async createEntityIndex(entity, field, indexType = 'btree') {
295
+ try {
296
+ const schemaName = this.searchClient.safeName(this.appId);
297
+ const tableName = `${schemaName}.${this.searchClient.safeName('jobs')}`;
298
+ const indexName = `idx_${this.appId}_${entity}_${field}`.replace(/[^a-zA-Z0-9_]/g, '_');
299
+ let sql;
300
+ if (indexType === 'gin') {
301
+ // GIN index for JSONB operations
302
+ sql = `
303
+ CREATE INDEX IF NOT EXISTS ${indexName}
304
+ ON ${tableName} USING gin (context jsonb_path_ops)
305
+ WHERE entity = '${entity}'
306
+ `;
307
+ }
308
+ else if (indexType === 'gist') {
309
+ // GiST index for specific field
310
+ sql = `
311
+ CREATE EXTENSION IF NOT EXISTS pg_trgm;
312
+ CREATE INDEX IF NOT EXISTS ${indexName}
313
+ ON ${tableName} USING gist ((context->>'${field}') gist_trgm_ops)
314
+ WHERE entity = '${entity}'
315
+ `;
316
+ }
317
+ else {
318
+ // B-tree index for specific field
319
+ sql = `
320
+ CREATE INDEX IF NOT EXISTS ${indexName}
321
+ ON ${tableName} USING btree ((context->>'${field}'))
322
+ WHERE entity = '${entity}'
323
+ `;
324
+ }
325
+ await this.pgClient.query(sql);
326
+ this.logger.info(`postgres-entity-index-created`, { entity, field, indexType, indexName });
327
+ }
328
+ catch (error) {
329
+ this.logger.error(`postgres-create-entity-index-error`, {
330
+ entity, field, indexType, error
331
+ });
332
+ throw error;
333
+ }
334
+ }
335
+ // Helper methods for entity operations
336
+ mongoToSqlOperator(mongoOp) {
337
+ const mapping = {
338
+ '$eq': '=',
339
+ '$ne': '!=',
340
+ '$gt': '>',
341
+ '$gte': '>=',
342
+ '$lt': '<',
343
+ '$lte': '<=',
344
+ '$in': 'IN',
345
+ };
346
+ return mapping[mongoOp] || '=';
347
+ }
348
+ inferType(value) {
349
+ if (typeof value === 'number') {
350
+ return Number.isInteger(value) ? 'integer' : 'numeric';
351
+ }
352
+ if (typeof value === 'boolean') {
353
+ return 'boolean';
354
+ }
355
+ return 'text';
356
+ }
148
357
  }
149
358
  exports.PostgresSearchService = PostgresSearchService;
@@ -15,5 +15,9 @@ declare class IORedisSearchService extends SearchService<IORedisClientType> {
15
15
  incrementFieldByFloat(key: string, field: string, increment: number): Promise<number>;
16
16
  sendQuery(...query: [string, ...string[]]): Promise<any>;
17
17
  sendIndexedQuery(index: string, query: string[]): Promise<string[]>;
18
+ findEntities(): Promise<any[]>;
19
+ findEntityById(): Promise<any>;
20
+ findEntitiesByCondition(): Promise<any[]>;
21
+ createEntityIndex(): Promise<void>;
18
22
  }
19
23
  export { IORedisSearchService };
@@ -117,5 +117,18 @@ class IORedisSearchService extends index_1.SearchService {
117
117
  throw error;
118
118
  }
119
119
  }
120
+ // Entity methods - not implemented for Redis (postgres-specific JSONB operations)
121
+ async findEntities() {
122
+ throw new Error('Entity findEntities not supported in Redis - use PostgreSQL');
123
+ }
124
+ async findEntityById() {
125
+ throw new Error('Entity findEntityById not supported in Redis - use PostgreSQL');
126
+ }
127
+ async findEntitiesByCondition() {
128
+ throw new Error('Entity findEntitiesByCondition not supported in Redis - use PostgreSQL');
129
+ }
130
+ async createEntityIndex() {
131
+ throw new Error('Entity createEntityIndex not supported in Redis - use PostgreSQL');
132
+ }
120
133
  }
121
134
  exports.IORedisSearchService = IORedisSearchService;
@@ -15,5 +15,9 @@ declare class RedisSearchService extends SearchService<RedisRedisClientType> {
15
15
  incrementFieldByFloat(key: string, field: string, increment: number): Promise<number>;
16
16
  sendQuery(...query: any[]): Promise<any>;
17
17
  sendIndexedQuery(index: string, query: string[]): Promise<string[]>;
18
+ findEntities(): Promise<any[]>;
19
+ findEntityById(): Promise<any>;
20
+ findEntitiesByCondition(): Promise<any[]>;
21
+ createEntityIndex(): Promise<void>;
18
22
  }
19
23
  export { RedisSearchService };
@@ -130,5 +130,18 @@ class RedisSearchService extends index_1.SearchService {
130
130
  throw error;
131
131
  }
132
132
  }
133
+ // Entity methods - not implemented for Redis (postgres-specific JSONB operations)
134
+ async findEntities() {
135
+ throw new Error('Entity findEntities not supported in Redis - use PostgreSQL');
136
+ }
137
+ async findEntityById() {
138
+ throw new Error('Entity findEntityById not supported in Redis - use PostgreSQL');
139
+ }
140
+ async findEntitiesByCondition() {
141
+ throw new Error('Entity findEntitiesByCondition not supported in Redis - use PostgreSQL');
142
+ }
143
+ async createEntityIndex() {
144
+ throw new Error('Entity createEntityIndex not supported in Redis - use PostgreSQL');
145
+ }
133
146
  }
134
147
  exports.RedisSearchService = RedisSearchService;