@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.
- package/README.md +234 -237
- package/build/modules/errors.d.ts +9 -0
- package/build/modules/errors.js +9 -0
- package/build/package.json +16 -14
- package/build/services/hotmesh/index.d.ts +9 -11
- package/build/services/hotmesh/index.js +9 -11
- package/build/services/memflow/entity.d.ts +168 -4
- package/build/services/memflow/entity.js +177 -15
- package/build/services/memflow/index.d.ts +8 -0
- package/build/services/memflow/index.js +8 -0
- package/build/services/memflow/worker.js +25 -0
- package/build/services/memflow/workflow/execChild.js +1 -0
- package/build/services/memflow/workflow/execHook.d.ts +2 -2
- package/build/services/memflow/workflow/execHook.js +19 -9
- package/build/services/memflow/workflow/index.d.ts +2 -4
- package/build/services/memflow/workflow/index.js +2 -4
- package/build/services/memflow/workflow/interruption.d.ts +28 -0
- package/build/services/memflow/workflow/interruption.js +43 -0
- package/build/services/memflow/workflow/proxyActivities.js +1 -0
- package/build/services/memflow/workflow/sleepFor.js +1 -0
- package/build/services/memflow/workflow/waitFor.js +4 -4
- package/build/services/search/index.d.ts +10 -0
- package/build/services/search/providers/postgres/postgres.d.ts +12 -0
- package/build/services/search/providers/postgres/postgres.js +209 -0
- package/build/services/search/providers/redis/ioredis.d.ts +4 -0
- package/build/services/search/providers/redis/ioredis.js +13 -0
- package/build/services/search/providers/redis/redis.d.ts +4 -0
- package/build/services/search/providers/redis/redis.js +13 -0
- package/build/services/store/providers/postgres/kvsql.d.ts +13 -37
- package/build/services/store/providers/postgres/kvsql.js +2 -2
- package/build/services/store/providers/postgres/kvtypes/hash/basic.d.ts +16 -0
- package/build/services/store/providers/postgres/kvtypes/hash/basic.js +480 -0
- package/build/services/store/providers/postgres/kvtypes/hash/expire.d.ts +5 -0
- package/build/services/store/providers/postgres/kvtypes/hash/expire.js +33 -0
- package/build/services/store/providers/postgres/kvtypes/hash/index.d.ts +29 -0
- package/build/services/store/providers/postgres/kvtypes/hash/index.js +190 -0
- package/build/services/store/providers/postgres/kvtypes/hash/jsonb.d.ts +14 -0
- package/build/services/store/providers/postgres/kvtypes/hash/jsonb.js +699 -0
- package/build/services/store/providers/postgres/kvtypes/hash/scan.d.ts +10 -0
- package/build/services/store/providers/postgres/kvtypes/hash/scan.js +91 -0
- package/build/services/store/providers/postgres/kvtypes/hash/types.d.ts +19 -0
- package/build/services/store/providers/postgres/kvtypes/hash/types.js +2 -0
- package/build/services/store/providers/postgres/kvtypes/hash/utils.d.ts +18 -0
- package/build/services/store/providers/postgres/kvtypes/hash/utils.js +90 -0
- package/build/types/memflow.d.ts +1 -1
- package/build/types/meshdata.d.ts +1 -1
- package/package.json +16 -14
- package/build/services/store/providers/postgres/kvtypes/hash.d.ts +0 -60
- 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
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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)();
|
|
@@ -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
|
-
|
|
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;
|