@hotmeshio/hotmesh 0.5.0 → 0.5.2

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 (49) hide show
  1. package/README.md +234 -237
  2. package/build/modules/errors.d.ts +9 -0
  3. package/build/modules/errors.js +9 -0
  4. package/build/package.json +16 -14
  5. package/build/services/hotmesh/index.d.ts +9 -11
  6. package/build/services/hotmesh/index.js +9 -11
  7. package/build/services/memflow/entity.d.ts +168 -4
  8. package/build/services/memflow/entity.js +177 -15
  9. package/build/services/memflow/index.d.ts +8 -0
  10. package/build/services/memflow/index.js +8 -0
  11. package/build/services/memflow/worker.js +25 -0
  12. package/build/services/memflow/workflow/execChild.js +1 -0
  13. package/build/services/memflow/workflow/execHook.d.ts +2 -2
  14. package/build/services/memflow/workflow/execHook.js +19 -9
  15. package/build/services/memflow/workflow/index.d.ts +2 -4
  16. package/build/services/memflow/workflow/index.js +2 -4
  17. package/build/services/memflow/workflow/interruption.d.ts +28 -0
  18. package/build/services/memflow/workflow/interruption.js +43 -0
  19. package/build/services/memflow/workflow/proxyActivities.js +1 -0
  20. package/build/services/memflow/workflow/sleepFor.js +1 -0
  21. package/build/services/memflow/workflow/waitFor.js +4 -4
  22. package/build/services/search/index.d.ts +10 -0
  23. package/build/services/search/providers/postgres/postgres.d.ts +12 -0
  24. package/build/services/search/providers/postgres/postgres.js +209 -0
  25. package/build/services/search/providers/redis/ioredis.d.ts +4 -0
  26. package/build/services/search/providers/redis/ioredis.js +13 -0
  27. package/build/services/search/providers/redis/redis.d.ts +4 -0
  28. package/build/services/search/providers/redis/redis.js +13 -0
  29. package/build/services/store/providers/postgres/kvsql.d.ts +13 -37
  30. package/build/services/store/providers/postgres/kvsql.js +2 -2
  31. package/build/services/store/providers/postgres/kvtypes/hash/basic.d.ts +16 -0
  32. package/build/services/store/providers/postgres/kvtypes/hash/basic.js +480 -0
  33. package/build/services/store/providers/postgres/kvtypes/hash/expire.d.ts +5 -0
  34. package/build/services/store/providers/postgres/kvtypes/hash/expire.js +33 -0
  35. package/build/services/store/providers/postgres/kvtypes/hash/index.d.ts +29 -0
  36. package/build/services/store/providers/postgres/kvtypes/hash/index.js +190 -0
  37. package/build/services/store/providers/postgres/kvtypes/hash/jsonb.d.ts +14 -0
  38. package/build/services/store/providers/postgres/kvtypes/hash/jsonb.js +699 -0
  39. package/build/services/store/providers/postgres/kvtypes/hash/scan.d.ts +10 -0
  40. package/build/services/store/providers/postgres/kvtypes/hash/scan.js +91 -0
  41. package/build/services/store/providers/postgres/kvtypes/hash/types.d.ts +19 -0
  42. package/build/services/store/providers/postgres/kvtypes/hash/types.js +2 -0
  43. package/build/services/store/providers/postgres/kvtypes/hash/utils.d.ts +18 -0
  44. package/build/services/store/providers/postgres/kvtypes/hash/utils.js +90 -0
  45. package/build/types/memflow.d.ts +1 -1
  46. package/build/types/meshdata.d.ts +1 -1
  47. package/package.json +16 -14
  48. package/build/services/store/providers/postgres/kvtypes/hash.d.ts +0 -60
  49. package/build/services/store/providers/postgres/kvtypes/hash.js +0 -1287
@@ -6,6 +6,7 @@ import { Entity } from './entity';
6
6
  import { WorkerService } from './worker';
7
7
  import { WorkflowService } from './workflow';
8
8
  import { WorkflowHandleService } from './handle';
9
+ import { didInterrupt } from './workflow/interruption';
9
10
  /**
10
11
  * The MemFlow service is a collection of services that
11
12
  * emulate Temporal's capabilities, but instead are
@@ -106,6 +107,13 @@ declare class MemFlowClass {
106
107
  * including: `execChild`, `waitFor`, `sleep`, etc
107
108
  */
108
109
  static workflow: typeof WorkflowService;
110
+ /**
111
+ * Checks if an error is a HotMesh reserved error type that indicates
112
+ * a workflow interruption rather than a true error condition.
113
+ *
114
+ * @see {@link utils/interruption.didInterrupt} for detailed documentation
115
+ */
116
+ static didInterrupt: typeof didInterrupt;
109
117
  /**
110
118
  * Shutdown everything. All connections, workers, and clients will be closed.
111
119
  * Include in your signal handlers to ensure a clean shutdown.
@@ -9,6 +9,7 @@ const entity_1 = require("./entity");
9
9
  const worker_1 = require("./worker");
10
10
  const workflow_1 = require("./workflow");
11
11
  const handle_1 = require("./handle");
12
+ const interruption_1 = require("./workflow/interruption");
12
13
  /**
13
14
  * The MemFlow service is a collection of services that
14
15
  * emulate Temporal's capabilities, but instead are
@@ -120,3 +121,10 @@ MemFlowClass.Worker = worker_1.WorkerService;
120
121
  * including: `execChild`, `waitFor`, `sleep`, etc
121
122
  */
122
123
  MemFlowClass.workflow = workflow_1.WorkflowService;
124
+ /**
125
+ * Checks if an error is a HotMesh reserved error type that indicates
126
+ * a workflow interruption rather than a true error condition.
127
+ *
128
+ * @see {@link utils/interruption.didInterrupt} for detailed documentation
129
+ */
130
+ MemFlowClass.didInterrupt = interruption_1.didInterrupt;
@@ -322,6 +322,31 @@ class WorkerService {
322
322
  const workflowResponse = await storage_1.asyncLocalStorage.run(context, async () => {
323
323
  return await workflowFunction.apply(this, workflowInput.arguments);
324
324
  });
325
+ //if the embedded function has a try/catch, it can interrup the throw
326
+ // throw here to interrupt the workflow if the embedded function caught and suppressed
327
+ if (interruptionRegistry.length > 0) {
328
+ const payload = interruptionRegistry[0];
329
+ switch (payload.type) {
330
+ case 'MemFlowWaitForError':
331
+ throw new errors_1.MemFlowWaitForError(payload);
332
+ case 'MemFlowProxyError':
333
+ throw new errors_1.MemFlowProxyError(payload);
334
+ case 'MemFlowChildError':
335
+ throw new errors_1.MemFlowChildError(payload);
336
+ case 'MemFlowSleepError':
337
+ throw new errors_1.MemFlowSleepError(payload);
338
+ case 'MemFlowTimeoutError':
339
+ throw new errors_1.MemFlowTimeoutError(payload.message, payload.stack);
340
+ case 'MemFlowMaxedError':
341
+ throw new errors_1.MemFlowMaxedError(payload.message, payload.stack);
342
+ case 'MemFlowFatalError':
343
+ throw new errors_1.MemFlowFatalError(payload.message, payload.stack);
344
+ case 'MemFlowRetryError':
345
+ throw new errors_1.MemFlowRetryError(payload.message, payload.stack);
346
+ default:
347
+ throw new errors_1.MemFlowRetryError(`Unknown interruption type: ${payload.type}`);
348
+ }
349
+ }
325
350
  return {
326
351
  code: 200,
327
352
  status: stream_1.StreamStatus.SUCCESS,
@@ -82,6 +82,7 @@ async function execChild(options) {
82
82
  const interruptionMessage = getChildInterruptPayload(context, options, execIndex);
83
83
  interruptionRegistry.push({
84
84
  code: common_1.HMSH_CODE_MEMFLOW_CHILD,
85
+ type: 'MemFlowChildError',
85
86
  ...interruptionMessage,
86
87
  });
87
88
  await (0, common_1.sleepImmediate)();
@@ -3,8 +3,8 @@ import { HookOptions } from './common';
3
3
  * Extended hook options that include signal configuration
4
4
  */
5
5
  export interface ExecHookOptions extends HookOptions {
6
- /** Signal ID to send after hook execution */
7
- signalId: string;
6
+ /** Signal ID to send after hook execution; if not provided, a random one will be generated */
7
+ signalId?: string;
8
8
  }
9
9
  /**
10
10
  * Executes a hook function and awaits the signal response.
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.execHook = void 0;
4
4
  const hook_1 = require("./hook");
5
5
  const waitFor_1 = require("./waitFor");
6
+ const interruption_1 = require("./interruption");
6
7
  /**
7
8
  * Executes a hook function and awaits the signal response.
8
9
  * This is a convenience method that combines `hook()` and `waitFor()` operations.
@@ -60,14 +61,23 @@ const waitFor_1 = require("./waitFor");
60
61
  * ```
61
62
  */
62
63
  async function execHook(options) {
63
- // Create hook options with signal field added to args
64
- const hookOptions = {
65
- ...options,
66
- args: [...options.args, { signal: options.signalId }]
67
- };
68
- // Execute the hook with the signal information
69
- await (0, hook_1.hook)(hookOptions);
70
- // Wait for the signal response and return it
71
- return await (0, waitFor_1.waitFor)(options.signalId);
64
+ try {
65
+ if (!options.signalId) {
66
+ options.signalId = 'memflow-hook-' + crypto.randomUUID();
67
+ }
68
+ const hookOptions = {
69
+ ...options,
70
+ args: [...options.args, { signal: options.signalId, $memflow: true }]
71
+ };
72
+ // Execute the hook with the signal information
73
+ await (0, hook_1.hook)(hookOptions);
74
+ // Wait for the signal response and return it
75
+ return await (0, waitFor_1.waitFor)(options.signalId);
76
+ }
77
+ catch (error) {
78
+ if ((0, interruption_1.didInterrupt)(error)) {
79
+ throw error;
80
+ }
81
+ }
72
82
  }
73
83
  exports.execHook = execHook;
@@ -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;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Checks if an error is a HotMesh reserved error type that indicates
3
+ * a HotMesh interruption rather than a true error condition.
4
+ *
5
+ * When this returns true, you can safely return rethrow the error.
6
+ * The workflow engine will handle the interruption automatically.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { MemFlow } from '@hotmeshio/hotmesh';
11
+ *
12
+ * try {
13
+ * await someWorkflowOperation();
14
+ * } catch (error) {
15
+ * // Check if this is a HotMesh interruption
16
+ * if (MemFlow.workflow.didInterrupt(error)) {
17
+ * // Rethrow the error
18
+ * throw error;
19
+ * }
20
+ * // Handle actual error
21
+ * console.error('Workflow failed:', error);
22
+ * }
23
+ * ```
24
+ *
25
+ * @param error - The error to check
26
+ * @returns true if the error is a HotMesh interruption
27
+ */
28
+ export declare function didInterrupt(error: Error): boolean;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.didInterrupt = void 0;
4
+ const errors_1 = require("../../../modules/errors");
5
+ /**
6
+ * Checks if an error is a HotMesh reserved error type that indicates
7
+ * a HotMesh interruption rather than a true error condition.
8
+ *
9
+ * When this returns true, you can safely return rethrow the error.
10
+ * The workflow engine will handle the interruption automatically.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { MemFlow } from '@hotmeshio/hotmesh';
15
+ *
16
+ * try {
17
+ * await someWorkflowOperation();
18
+ * } catch (error) {
19
+ * // Check if this is a HotMesh interruption
20
+ * if (MemFlow.workflow.didInterrupt(error)) {
21
+ * // Rethrow the error
22
+ * throw error;
23
+ * }
24
+ * // Handle actual error
25
+ * console.error('Workflow failed:', error);
26
+ * }
27
+ * ```
28
+ *
29
+ * @param error - The error to check
30
+ * @returns true if the error is a HotMesh interruption
31
+ */
32
+ function didInterrupt(error) {
33
+ return (error instanceof errors_1.MemFlowChildError ||
34
+ error instanceof errors_1.MemFlowFatalError ||
35
+ error instanceof errors_1.MemFlowMaxedError ||
36
+ error instanceof errors_1.MemFlowProxyError ||
37
+ error instanceof errors_1.MemFlowRetryError ||
38
+ error instanceof errors_1.MemFlowSleepError ||
39
+ error instanceof errors_1.MemFlowTimeoutError ||
40
+ error instanceof errors_1.MemFlowWaitForError ||
41
+ error instanceof errors_1.MemFlowWaitForAllError);
42
+ }
43
+ exports.didInterrupt = didInterrupt;
@@ -67,6 +67,7 @@ function wrapActivity(activityName, options) {
67
67
  const interruptionMessage = getProxyInterruptPayload(context, activityName, execIndex, args, options);
68
68
  interruptionRegistry.push({
69
69
  code: common_1.HMSH_CODE_MEMFLOW_PROXY,
70
+ type: 'MemFlowProxyError',
70
71
  ...interruptionMessage,
71
72
  });
72
73
  await (0, common_1.sleepImmediate)();
@@ -43,6 +43,7 @@ async function sleepFor(duration) {
43
43
  };
44
44
  interruptionRegistry.push({
45
45
  code: common_1.HMSH_CODE_MEMFLOW_SLEEP,
46
+ type: 'MemFlowSleepError',
46
47
  ...interruptionMessage,
47
48
  });
48
49
  await (0, common_1.sleepImmediate)();
@@ -45,12 +45,12 @@ async function waitFor(signalId) {
45
45
  signalId,
46
46
  index: execIndex,
47
47
  workflowDimension,
48
- };
49
- interruptionRegistry.push({
48
+ type: 'MemFlowWaitForError',
50
49
  code: common_1.HMSH_CODE_MEMFLOW_WAIT,
51
- ...interruptionMessage,
52
- });
50
+ };
51
+ interruptionRegistry.push(interruptionMessage);
53
52
  await (0, common_1.sleepImmediate)();
53
+ //if you are seeing this error in the logs, you might have forgotten to `await waitFor(...)`
54
54
  throw new common_1.MemFlowWaitForError(interruptionMessage);
55
55
  }
56
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;