@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.
- package/README.md +37 -48
- 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/workflow/index.d.ts +2 -4
- package/build/services/memflow/workflow/index.js +2 -4
- package/build/services/memflow/workflow/interruption.d.ts +6 -4
- package/build/services/memflow/workflow/interruption.js +6 -4
- package/build/services/memflow/workflow/waitFor.js +1 -0
- 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
|
@@ -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
|
|
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
|
-
*
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
|
3
|
+
* a HotMesh interruption rather than a true error condition.
|
|
4
4
|
*
|
|
5
|
-
* When this returns true, you can safely return
|
|
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
|
|
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
|
|
7
|
+
* a HotMesh interruption rather than a true error condition.
|
|
8
8
|
*
|
|
9
|
-
* When this returns true, you can safely return
|
|
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
|
|
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;
|