@path58/n8n-mcp 0.1.0
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/AGENT_INSTALL.md +223 -0
- package/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/README.md +187 -0
- package/dist/autofix/suggestion-fixers/deprecated-node-fixer.js +465 -0
- package/dist/autofix/suggestion-fixers/deprecated-node-fixer.js.map +1 -0
- package/dist/autofix/suggestion-fixers/fixer-registry.js +495 -0
- package/dist/autofix/suggestion-fixers/fixer-registry.js.map +1 -0
- package/dist/autofix/suggestion-fixers/l1-structure-fixer.js +639 -0
- package/dist/autofix/suggestion-fixers/l1-structure-fixer.js.map +1 -0
- package/dist/autofix/suggestion-fixers/l4-connection-fixer.js +449 -0
- package/dist/autofix/suggestion-fixers/l4-connection-fixer.js.map +1 -0
- package/dist/autofix/suggestion-fixers/l5-parameter-fixer.js +575 -0
- package/dist/autofix/suggestion-fixers/l5-parameter-fixer.js.map +1 -0
- package/dist/autofix/suggestion-fixers/l5-typeversion-fixer.js +431 -0
- package/dist/autofix/suggestion-fixers/l5-typeversion-fixer.js.map +1 -0
- package/dist/autofix/suggestion-fixers/l5-webhook-path-fixer.js +356 -0
- package/dist/autofix/suggestion-fixers/l5-webhook-path-fixer.js.map +1 -0
- package/dist/autofix/suggestion-fixers/l6-ai-tool-variant-fixer.js +618 -0
- package/dist/autofix/suggestion-fixers/l6-ai-tool-variant-fixer.js.map +1 -0
- package/dist/autofix/suggestion-fixers/l6-pattern-fixer.js +1475 -0
- package/dist/autofix/suggestion-fixers/l6-pattern-fixer.js.map +1 -0
- package/dist/autofix/suggestion-fixers/llm-fixer.js +716 -0
- package/dist/autofix/suggestion-fixers/llm-fixer.js.map +1 -0
- package/dist/autofix/suggestion-fixers/missing-credential-fixer.js +336 -0
- package/dist/autofix/suggestion-fixers/missing-credential-fixer.js.map +1 -0
- package/dist/autofix/suggestion-fixers/types.js +29 -0
- package/dist/autofix/suggestion-fixers/types.js.map +1 -0
- package/dist/autofix/suggestion-fixers/typo-fixer.js +197 -0
- package/dist/autofix/suggestion-fixers/typo-fixer.js.map +1 -0
- package/dist/classification/certification-engine.js +208 -0
- package/dist/classification/certification-engine.js.map +1 -0
- package/dist/classification/feedback-collector.js +516 -0
- package/dist/classification/feedback-collector.js.map +1 -0
- package/dist/classification/l5-parameter-analyzer.js +670 -0
- package/dist/classification/l5-parameter-analyzer.js.map +1 -0
- package/dist/classification/l6-graph-analyzer.js +613 -0
- package/dist/classification/l6-graph-analyzer.js.map +1 -0
- package/dist/classification/severity-classifier.js +237 -0
- package/dist/classification/severity-classifier.js.map +1 -0
- package/dist/config/env.js +280 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/env.schema.js +234 -0
- package/dist/config/env.schema.js.map +1 -0
- package/dist/config/scraperEnv.js +55 -0
- package/dist/config/scraperEnv.js.map +1 -0
- package/dist/db/postgresClient.js +38 -0
- package/dist/db/postgresClient.js.map +1 -0
- package/dist/db/scraperDb.js +6 -0
- package/dist/db/scraperDb.js.map +1 -0
- package/dist/db/scraperPostgresClient.js +118 -0
- package/dist/db/scraperPostgresClient.js.map +1 -0
- package/dist/db/validationRepository.js +55 -0
- package/dist/db/validationRepository.js.map +1 -0
- package/dist/db/validatorPostgresClient.js +248 -0
- package/dist/db/validatorPostgresClient.js.map +1 -0
- package/dist/db/workflowInstanceMappingRepository.js +128 -0
- package/dist/db/workflowInstanceMappingRepository.js.map +1 -0
- package/dist/errors/AppError.js +156 -0
- package/dist/errors/AppError.js.map +1 -0
- package/dist/errors/index.js +7 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/factory/error-to-problem-mappers.js +385 -0
- package/dist/factory/error-to-problem-mappers.js.map +1 -0
- package/dist/factory/gap-recorder.js +260 -0
- package/dist/factory/gap-recorder.js.map +1 -0
- package/dist/factory/problem-recorder.js +94 -0
- package/dist/factory/problem-recorder.js.map +1 -0
- package/dist/factory/warning-to-gap-mappers.js +493 -0
- package/dist/factory/warning-to-gap-mappers.js.map +1 -0
- package/dist/factory/workflow-normalizer.js +247 -0
- package/dist/factory/workflow-normalizer.js.map +1 -0
- package/dist/mcp/adapters/catalog.js +13 -0
- package/dist/mcp/adapters/catalog.js.map +1 -0
- package/dist/mcp/adapters/index.js +36 -0
- package/dist/mcp/adapters/index.js.map +1 -0
- package/dist/mcp/adapters/supabase-catalog.js +467 -0
- package/dist/mcp/adapters/supabase-catalog.js.map +1 -0
- package/dist/mcp/adapters/test-catalog-adapter.js +100 -0
- package/dist/mcp/adapters/test-catalog-adapter.js.map +1 -0
- package/dist/mcp/adapters/validation.js +258 -0
- package/dist/mcp/adapters/validation.js.map +1 -0
- package/dist/mcp/build-email-workflow.js +113 -0
- package/dist/mcp/build-email-workflow.js.map +1 -0
- package/dist/mcp/config.js +22 -0
- package/dist/mcp/config.js.map +1 -0
- package/dist/mcp/formatters/errors.js +217 -0
- package/dist/mcp/formatters/errors.js.map +1 -0
- package/dist/mcp/formatters/index.js +12 -0
- package/dist/mcp/formatters/index.js.map +1 -0
- package/dist/mcp/formatters/response.js +141 -0
- package/dist/mcp/formatters/response.js.map +1 -0
- package/dist/mcp/quick-test.js +33 -0
- package/dist/mcp/quick-test.js.map +1 -0
- package/dist/mcp/server.js +70 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/test-mcp-error.js +81 -0
- package/dist/mcp/test-mcp-error.js.map +1 -0
- package/dist/mcp/test-mcp.js +80 -0
- package/dist/mcp/test-mcp.js.map +1 -0
- package/dist/mcp/tools/fixes/expression-fixes.js +166 -0
- package/dist/mcp/tools/fixes/expression-fixes.js.map +1 -0
- package/dist/mcp/tools/fixes/flow-fixes.js +155 -0
- package/dist/mcp/tools/fixes/flow-fixes.js.map +1 -0
- package/dist/mcp/tools/fixes/index.js +91 -0
- package/dist/mcp/tools/fixes/index.js.map +1 -0
- package/dist/mcp/tools/fixes/node-fixes.js +233 -0
- package/dist/mcp/tools/fixes/node-fixes.js.map +1 -0
- package/dist/mcp/tools/fixes/parameter-fixes.js +277 -0
- package/dist/mcp/tools/fixes/parameter-fixes.js.map +1 -0
- package/dist/mcp/tools/fixes/types.js +10 -0
- package/dist/mcp/tools/fixes/types.js.map +1 -0
- package/dist/mcp/tools/handlers/check-parameter.js +300 -0
- package/dist/mcp/tools/handlers/check-parameter.js.map +1 -0
- package/dist/mcp/tools/handlers/find-similar-pattern.js +121 -0
- package/dist/mcp/tools/handlers/find-similar-pattern.js.map +1 -0
- package/dist/mcp/tools/handlers/get-node-info.js +131 -0
- package/dist/mcp/tools/handlers/get-node-info.js.map +1 -0
- package/dist/mcp/tools/handlers/get-operation-schema.js +141 -0
- package/dist/mcp/tools/handlers/get-operation-schema.js.map +1 -0
- package/dist/mcp/tools/handlers/list-nodes.js +126 -0
- package/dist/mcp/tools/handlers/list-nodes.js.map +1 -0
- package/dist/mcp/tools/handlers/list-operations.js +138 -0
- package/dist/mcp/tools/handlers/list-operations.js.map +1 -0
- package/dist/mcp/tools/handlers/suggest-fix.js +120 -0
- package/dist/mcp/tools/handlers/suggest-fix.js.map +1 -0
- package/dist/mcp/tools/handlers/validate-workflow.js +92 -0
- package/dist/mcp/tools/handlers/validate-workflow.js.map +1 -0
- package/dist/mcp/tools/index.js +190 -0
- package/dist/mcp/tools/index.js.map +1 -0
- package/dist/mcp/tools/schemas.js +195 -0
- package/dist/mcp/tools/schemas.js.map +1 -0
- package/dist/mcp/tools/validate.js +95 -0
- package/dist/mcp/tools/validate.js.map +1 -0
- package/dist/mcp/types/mcp.js +7 -0
- package/dist/mcp/types/mcp.js.map +1 -0
- package/dist/mcp/utils/timeout.js +78 -0
- package/dist/mcp/utils/timeout.js.map +1 -0
- package/dist/services/BatchProcessor.js +433 -0
- package/dist/services/BatchProcessor.js.map +1 -0
- package/dist/services/CheckpointManager.js +281 -0
- package/dist/services/CheckpointManager.js.map +1 -0
- package/dist/services/CostCalculator.js +211 -0
- package/dist/services/CostCalculator.js.map +1 -0
- package/dist/services/EmbeddingCache.js +68 -0
- package/dist/services/EmbeddingCache.js.map +1 -0
- package/dist/services/EmbeddingService.js +143 -0
- package/dist/services/EmbeddingService.js.map +1 -0
- package/dist/services/RankingService.js +81 -0
- package/dist/services/RankingService.js.map +1 -0
- package/dist/services/RedisCache.js +376 -0
- package/dist/services/RedisCache.js.map +1 -0
- package/dist/services/RedisCatalogCache.js +680 -0
- package/dist/services/RedisCatalogCache.js.map +1 -0
- package/dist/services/ResumeManager.js +252 -0
- package/dist/services/ResumeManager.js.map +1 -0
- package/dist/services/SearchService.js +282 -0
- package/dist/services/SearchService.js.map +1 -0
- package/dist/services/SemanticCatalogSearch.js +405 -0
- package/dist/services/SemanticCatalogSearch.js.map +1 -0
- package/dist/services/ValidationCache.js +157 -0
- package/dist/services/ValidationCache.js.map +1 -0
- package/dist/services/WorkflowPipelineService.js +1997 -0
- package/dist/services/WorkflowPipelineService.js.map +1 -0
- package/dist/services/catalog/index.js +34 -0
- package/dist/services/catalog/index.js.map +1 -0
- package/dist/services/catalog/interfaces.js +17 -0
- package/dist/services/catalog/interfaces.js.map +1 -0
- package/dist/services/catalog/loaders.js +169 -0
- package/dist/services/catalog/loaders.js.map +1 -0
- package/dist/services/catalog/types.js +138 -0
- package/dist/services/catalog/types.js.map +1 -0
- package/dist/services/documentation-normalization/docUrlUtils.js +88 -0
- package/dist/services/documentation-normalization/docUrlUtils.js.map +1 -0
- package/dist/services/error-quality/ErrorQualityService.js +262 -0
- package/dist/services/error-quality/ErrorQualityService.js.map +1 -0
- package/dist/services/error-quality/analyzers/CredentialAnalyzer.js +260 -0
- package/dist/services/error-quality/analyzers/CredentialAnalyzer.js.map +1 -0
- package/dist/services/error-quality/analyzers/IssuePredictor.js +380 -0
- package/dist/services/error-quality/analyzers/IssuePredictor.js.map +1 -0
- package/dist/services/error-quality/analyzers/MockCoverageAnalyzer.js +267 -0
- package/dist/services/error-quality/analyzers/MockCoverageAnalyzer.js.map +1 -0
- package/dist/services/error-quality/data/ErrorPatternSeeder.js +963 -0
- package/dist/services/error-quality/data/ErrorPatternSeeder.js.map +1 -0
- package/dist/services/error-quality/index.js +25 -0
- package/dist/services/error-quality/index.js.map +1 -0
- package/dist/services/error-quality/reports/ReportGenerator.js +343 -0
- package/dist/services/error-quality/reports/ReportGenerator.js.map +1 -0
- package/dist/services/error-quality/taxonomy/ErrorTaxonomy.js +698 -0
- package/dist/services/error-quality/taxonomy/ErrorTaxonomy.js.map +1 -0
- package/dist/services/error-quality/types.js +11 -0
- package/dist/services/error-quality/types.js.map +1 -0
- package/dist/services/progress/ProgressTracker.js +288 -0
- package/dist/services/progress/ProgressTracker.js.map +1 -0
- package/dist/services/progress/formatters.js +122 -0
- package/dist/services/progress/formatters.js.map +1 -0
- package/dist/services/progress/index.js +36 -0
- package/dist/services/progress/index.js.map +1 -0
- package/dist/services/progress/types.js +7 -0
- package/dist/services/progress/types.js.map +1 -0
- package/dist/services/search/embeddingGenerator.js +112 -0
- package/dist/services/search/embeddingGenerator.js.map +1 -0
- package/dist/types/aiCapabilities.js +7 -0
- package/dist/types/aiCapabilities.js.map +1 -0
- package/dist/types/aiConfigSchema.js +7 -0
- package/dist/types/aiConfigSchema.js.map +1 -0
- package/dist/utils/bannerLogger.js +186 -0
- package/dist/utils/bannerLogger.js.map +1 -0
- package/dist/utils/bannerService.js +23 -0
- package/dist/utils/bannerService.js.map +1 -0
- package/dist/utils/bannerServiceAdapter.js +54 -0
- package/dist/utils/bannerServiceAdapter.js.map +1 -0
- package/dist/utils/batchLogger.js +171 -0
- package/dist/utils/batchLogger.js.map +1 -0
- package/dist/utils/bottomStickyBanner.js +239 -0
- package/dist/utils/bottomStickyBanner.js.map +1 -0
- package/dist/utils/credentialMatcher.js +206 -0
- package/dist/utils/credentialMatcher.js.map +1 -0
- package/dist/utils/credentialNormalizer.js +442 -0
- package/dist/utils/credentialNormalizer.js.map +1 -0
- package/dist/utils/integratedBannerLogger.js +59 -0
- package/dist/utils/integratedBannerLogger.js.map +1 -0
- package/dist/utils/n8nSourceGit.js +195 -0
- package/dist/utils/n8nSourceGit.js.map +1 -0
- package/dist/utils/nodeTypeNormalizer.js +131 -0
- package/dist/utils/nodeTypeNormalizer.js.map +1 -0
- package/dist/utils/openaiClient.js +397 -0
- package/dist/utils/openaiClient.js.map +1 -0
- package/dist/utils/productionLogger.js +16 -0
- package/dist/utils/productionLogger.js.map +1 -0
- package/dist/utils/progressBarBanner.js +132 -0
- package/dist/utils/progressBarBanner.js.map +1 -0
- package/dist/utils/scriptHeartbeat.js +117 -0
- package/dist/utils/scriptHeartbeat.js.map +1 -0
- package/dist/utils/scriptLogger.js +125 -0
- package/dist/utils/scriptLogger.js.map +1 -0
- package/dist/utils/scriptRunner.js +95 -0
- package/dist/utils/scriptRunner.js.map +1 -0
- package/dist/utils/scriptTimeout.js +128 -0
- package/dist/utils/scriptTimeout.js.map +1 -0
- package/dist/utils/scriptWrapper.js +219 -0
- package/dist/utils/scriptWrapper.js.map +1 -0
- package/dist/utils/stickyBanner.js +226 -0
- package/dist/utils/stickyBanner.js.map +1 -0
- package/dist/utils/terminalSpinner.js +97 -0
- package/dist/utils/terminalSpinner.js.map +1 -0
- package/dist/utils/threeLineBanner.js +427 -0
- package/dist/utils/threeLineBanner.js.map +1 -0
- package/dist/utils/validatorCheckpointManager.js +170 -0
- package/dist/utils/validatorCheckpointManager.js.map +1 -0
- package/dist/utils/validatorConnectionManager.js +124 -0
- package/dist/utils/validatorConnectionManager.js.map +1 -0
- package/dist/validation/catalog.js +56 -0
- package/dist/validation/catalog.js.map +1 -0
- package/dist/validation/config/deprecated-nodes.js +234 -0
- package/dist/validation/config/deprecated-nodes.js.map +1 -0
- package/dist/validation/config/l6-severity.js +227 -0
- package/dist/validation/config/l6-severity.js.map +1 -0
- package/dist/validation/config/terminal-nodes.js +132 -0
- package/dist/validation/config/terminal-nodes.js.map +1 -0
- package/dist/validation/config/unreachable-nodes.js +67 -0
- package/dist/validation/config/unreachable-nodes.js.map +1 -0
- package/dist/validation/core.js +47 -0
- package/dist/validation/core.js.map +1 -0
- package/dist/validation/docExtraction.js +12 -0
- package/dist/validation/docExtraction.js.map +1 -0
- package/dist/validation/dryRunMockRunner.js +128 -0
- package/dist/validation/dryRunMockRunner.js.map +1 -0
- package/dist/validation/fixtureEngine.js +61 -0
- package/dist/validation/fixtureEngine.js.map +1 -0
- package/dist/validation/index.js +15 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/validation/k-levels/k2-blockers.js +222 -0
- package/dist/validation/k-levels/k2-blockers.js.map +1 -0
- package/dist/validation/l1-structure.js +296 -0
- package/dist/validation/l1-structure.js.map +1 -0
- package/dist/validation/l2-nodes.js +282 -0
- package/dist/validation/l2-nodes.js.map +1 -0
- package/dist/validation/l3-credentials.js +322 -0
- package/dist/validation/l3-credentials.js.map +1 -0
- package/dist/validation/l4-connections.js +698 -0
- package/dist/validation/l4-connections.js.map +1 -0
- package/dist/validation/l5-parameters.js +803 -0
- package/dist/validation/l5-parameters.js.map +1 -0
- package/dist/validation/l6-checks/ai-tool-variants.js +407 -0
- package/dist/validation/l6-checks/ai-tool-variants.js.map +1 -0
- package/dist/validation/l6-checks/catalog-checks.js +260 -0
- package/dist/validation/l6-checks/catalog-checks.js.map +1 -0
- package/dist/validation/l6-checks/data-contracts.js +197 -0
- package/dist/validation/l6-checks/data-contracts.js.map +1 -0
- package/dist/validation/l6-checks/deprecation.js +133 -0
- package/dist/validation/l6-checks/deprecation.js.map +1 -0
- package/dist/validation/l6-checks/error-handling.js +193 -0
- package/dist/validation/l6-checks/error-handling.js.map +1 -0
- package/dist/validation/l6-checks/expression-syntax.js +387 -0
- package/dist/validation/l6-checks/expression-syntax.js.map +1 -0
- package/dist/validation/l6-checks/flow-integrity.js +504 -0
- package/dist/validation/l6-checks/flow-integrity.js.map +1 -0
- package/dist/validation/l6-checks/index.js +106 -0
- package/dist/validation/l6-checks/index.js.map +1 -0
- package/dist/validation/l6-checks/loops.js +370 -0
- package/dist/validation/l6-checks/loops.js.map +1 -0
- package/dist/validation/l6-checks/performance.js +182 -0
- package/dist/validation/l6-checks/performance.js.map +1 -0
- package/dist/validation/l6-checks/security.js +273 -0
- package/dist/validation/l6-checks/security.js.map +1 -0
- package/dist/validation/l6-patterns.js +472 -0
- package/dist/validation/l6-patterns.js.map +1 -0
- package/dist/validation/mockLevelResolver.js +95 -0
- package/dist/validation/mockLevelResolver.js.map +1 -0
- package/dist/validation/n8nApiClient.js +21 -0
- package/dist/validation/n8nApiClient.js.map +1 -0
- package/dist/validation/n8nCli.js +87 -0
- package/dist/validation/n8nCli.js.map +1 -0
- package/dist/validation/types.js +8 -0
- package/dist/validation/types.js.map +1 -0
- package/dist/validation/usageStats.js +82 -0
- package/dist/validation/usageStats.js.map +1 -0
- package/package.json +274 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis Catalog Cache Service
|
|
3
|
+
*
|
|
4
|
+
* Implements ICatalogCache using Redis for distributed caching.
|
|
5
|
+
* Caches all bluelime catalog data (nodes, operations, parameters,
|
|
6
|
+
* credentials, connection_rules) with structured key namespaces.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Sub-millisecond lookups for cached data
|
|
10
|
+
* - Graceful fallback to DB if Redis unavailable
|
|
11
|
+
* - Configurable TTL (default: 24 hours)
|
|
12
|
+
* - Hit/miss tracking for monitoring
|
|
13
|
+
*
|
|
14
|
+
* @module services/RedisCatalogCache
|
|
15
|
+
*/
|
|
16
|
+
import Redis from 'ioredis';
|
|
17
|
+
import { logger } from '@tsvika58/shared-utilities/logging';
|
|
18
|
+
import { loadFullCatalog } from './catalog/loaders.js';
|
|
19
|
+
import { validatorQuery } from '../db/validatorPostgresClient.js';
|
|
20
|
+
/** Default TTL in seconds (24 hours) */
|
|
21
|
+
const DEFAULT_TTL = 86400;
|
|
22
|
+
/** Redis key prefix for all catalog data */
|
|
23
|
+
const PREFIX = 'catalog:';
|
|
24
|
+
/**
|
|
25
|
+
* Normalize node type to lowercase for consistent lookups.
|
|
26
|
+
*/
|
|
27
|
+
function normalizeNodeType(nodeType) {
|
|
28
|
+
return nodeType.toLowerCase().trim();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* RedisCatalogCache implements ICatalogCache using Redis as the backing store.
|
|
32
|
+
* Falls back to database queries when Redis is unavailable.
|
|
33
|
+
*/
|
|
34
|
+
export class RedisCatalogCache {
|
|
35
|
+
redis = null;
|
|
36
|
+
isConnected = false;
|
|
37
|
+
ready = false;
|
|
38
|
+
config;
|
|
39
|
+
stats;
|
|
40
|
+
constructor(config) {
|
|
41
|
+
this.config = {
|
|
42
|
+
ttlSeconds: config?.ttlSeconds ?? DEFAULT_TTL,
|
|
43
|
+
autoRefresh: config?.autoRefresh ?? false,
|
|
44
|
+
refreshIntervalSeconds: config?.refreshIntervalSeconds ?? 3600,
|
|
45
|
+
preloadOnInit: config?.preloadOnInit ?? true,
|
|
46
|
+
redisUrl: config?.redisUrl ?? process.env.REDIS_URL ?? '',
|
|
47
|
+
fallbackToMemory: config?.fallbackToMemory ?? true,
|
|
48
|
+
};
|
|
49
|
+
this.stats = {
|
|
50
|
+
nodes: 0,
|
|
51
|
+
operations: 0,
|
|
52
|
+
parameters: 0,
|
|
53
|
+
credentials: 0,
|
|
54
|
+
connections: 0,
|
|
55
|
+
loadedAt: null,
|
|
56
|
+
loadDurationMs: 0,
|
|
57
|
+
hits: 0,
|
|
58
|
+
misses: 0,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// ============================================================
|
|
62
|
+
// Lifecycle Methods
|
|
63
|
+
// ============================================================
|
|
64
|
+
async initialize() {
|
|
65
|
+
await this.connectRedis();
|
|
66
|
+
if (this.config.preloadOnInit && this.isConnected && this.redis) {
|
|
67
|
+
// Check if Redis already has cached data (skip redundant Supabase loads)
|
|
68
|
+
// RAG-2.2.93.13: Check nodes, operations, AND parameters to ensure cache is complete
|
|
69
|
+
try {
|
|
70
|
+
const cachedNodeCount = await this.redis.scard(`${PREFIX}nodes:_all`);
|
|
71
|
+
// Check if operations are cached
|
|
72
|
+
const opKeys = await this.redis.keys(`${PREFIX}operations:*`);
|
|
73
|
+
const hasOperations = opKeys.length > 100; // Expect many operation keys
|
|
74
|
+
// Check if parameters are cached
|
|
75
|
+
const paramKeys = await this.redis.keys(`${PREFIX}parameters:*`);
|
|
76
|
+
const hasParameters = paramKeys.length > 100; // Expect many parameter keys
|
|
77
|
+
if (cachedNodeCount > 0 && hasOperations && hasParameters) {
|
|
78
|
+
// Cache already populated - restore stats from Redis
|
|
79
|
+
this.stats.nodes = cachedNodeCount;
|
|
80
|
+
this.stats.operations = opKeys.length;
|
|
81
|
+
this.stats.parameters = paramKeys.length;
|
|
82
|
+
this.stats.credentials = await this.redis.scard(`${PREFIX}credentials:_all`);
|
|
83
|
+
logger.info('Using existing Redis cache', {
|
|
84
|
+
node_count: cachedNodeCount,
|
|
85
|
+
op_keys: opKeys.length,
|
|
86
|
+
param_keys: paramKeys.length,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
// Cache incomplete - need to load from Supabase
|
|
91
|
+
logger.info('Redis cache incomplete, refreshing...', {
|
|
92
|
+
nodes: cachedNodeCount,
|
|
93
|
+
op_keys: opKeys.length,
|
|
94
|
+
param_keys: paramKeys.length,
|
|
95
|
+
});
|
|
96
|
+
await this.refresh();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Fallback to refresh if cache check fails (e.g., mocked Redis in tests)
|
|
101
|
+
await this.refresh();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
this.ready = true;
|
|
105
|
+
}
|
|
106
|
+
async refresh() {
|
|
107
|
+
const start = performance.now();
|
|
108
|
+
try {
|
|
109
|
+
const catalogData = await loadFullCatalog();
|
|
110
|
+
await this.populateCache(catalogData);
|
|
111
|
+
this.stats.loadedAt = new Date().toISOString();
|
|
112
|
+
this.stats.loadDurationMs = performance.now() - start;
|
|
113
|
+
return {
|
|
114
|
+
success: true,
|
|
115
|
+
stats: await this.getStats(),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
120
|
+
return {
|
|
121
|
+
success: false,
|
|
122
|
+
stats: await this.getStats(),
|
|
123
|
+
error: errorMsg,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async getStats() {
|
|
128
|
+
return {
|
|
129
|
+
nodes: this.stats.nodes,
|
|
130
|
+
operations: this.stats.operations,
|
|
131
|
+
parameters: this.stats.parameters,
|
|
132
|
+
credentials: this.stats.credentials,
|
|
133
|
+
connections: this.stats.connections,
|
|
134
|
+
loaded_at: this.stats.loadedAt,
|
|
135
|
+
load_duration_ms: this.stats.loadDurationMs,
|
|
136
|
+
hits: this.stats.hits,
|
|
137
|
+
misses: this.stats.misses,
|
|
138
|
+
backend: this.getBackend(),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
isReady() {
|
|
142
|
+
return this.ready;
|
|
143
|
+
}
|
|
144
|
+
async dispose() {
|
|
145
|
+
if (this.redis) {
|
|
146
|
+
await this.redis.quit();
|
|
147
|
+
this.redis = null;
|
|
148
|
+
this.isConnected = false;
|
|
149
|
+
}
|
|
150
|
+
this.ready = false;
|
|
151
|
+
}
|
|
152
|
+
// ============================================================
|
|
153
|
+
// Node Operations
|
|
154
|
+
// ============================================================
|
|
155
|
+
async getNode(nodeType) {
|
|
156
|
+
const key = `${PREFIX}nodes:${normalizeNodeType(nodeType)}`;
|
|
157
|
+
return this.getCachedOrFallback(key, async () => {
|
|
158
|
+
const result = await validatorQuery('SELECT * FROM bluelime.nodes WHERE LOWER(node_type) = $1', [normalizeNodeType(nodeType)]);
|
|
159
|
+
return result.rows[0] ?? null;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
async isTrigger(nodeType) {
|
|
163
|
+
const node = await this.getNode(nodeType);
|
|
164
|
+
return node?.is_trigger ?? false;
|
|
165
|
+
}
|
|
166
|
+
async isDeprecated(nodeType) {
|
|
167
|
+
const node = await this.getNode(nodeType);
|
|
168
|
+
return node?.is_deprecated ?? false;
|
|
169
|
+
}
|
|
170
|
+
async getNodesByRole(role) {
|
|
171
|
+
const key = `${PREFIX}nodes:_roles:${role}`;
|
|
172
|
+
return this.getSetMembers(key);
|
|
173
|
+
}
|
|
174
|
+
async getAllNodeTypes() {
|
|
175
|
+
// Try Redis first
|
|
176
|
+
const cachedTypes = await this.getSetMembers(`${PREFIX}nodes:_all`);
|
|
177
|
+
if (cachedTypes.length > 0) {
|
|
178
|
+
return cachedTypes;
|
|
179
|
+
}
|
|
180
|
+
// Fallback to database query if Redis set is empty
|
|
181
|
+
try {
|
|
182
|
+
const result = await validatorQuery('SELECT LOWER(node_type) as node_type FROM bluelime.nodes');
|
|
183
|
+
return result.rows.map((row) => row.node_type);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async hasNode(nodeType) {
|
|
190
|
+
const key = `${PREFIX}nodes:${normalizeNodeType(nodeType)}`;
|
|
191
|
+
if (this.isConnected && this.redis) {
|
|
192
|
+
const exists = await this.redis.exists(key);
|
|
193
|
+
return exists === 1;
|
|
194
|
+
}
|
|
195
|
+
const node = await this.getNode(nodeType);
|
|
196
|
+
return node !== null;
|
|
197
|
+
}
|
|
198
|
+
// ============================================================
|
|
199
|
+
// Operation and Parameter Methods
|
|
200
|
+
// ============================================================
|
|
201
|
+
async getOperations(nodeType) {
|
|
202
|
+
const key = `${PREFIX}operations:${normalizeNodeType(nodeType)}`;
|
|
203
|
+
const result = await this.getCachedOrFallback(key, async () => {
|
|
204
|
+
const dbResult = await validatorQuery(`SELECT o.id, n.node_type, o.display_name AS operation_name,
|
|
205
|
+
o.operation AS operation_value, o.description, o.required_parameters
|
|
206
|
+
FROM bluelime.operations o
|
|
207
|
+
JOIN bluelime.nodes n ON o.node_id = n.id
|
|
208
|
+
WHERE LOWER(n.node_type) = $1`, [normalizeNodeType(nodeType)]);
|
|
209
|
+
return dbResult.rows;
|
|
210
|
+
});
|
|
211
|
+
return result ?? [];
|
|
212
|
+
}
|
|
213
|
+
async getOperation(nodeType, operationValue) {
|
|
214
|
+
const operations = await this.getOperations(nodeType);
|
|
215
|
+
return (operations.find((op) => op.operation_value.toLowerCase() === operationValue.toLowerCase()) ?? null);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* RAG-2.2.108.5: Batch fetch operations for multiple node types.
|
|
219
|
+
* Reduces Redis round-trips from O(n) to O(1) for L5 validation.
|
|
220
|
+
*/
|
|
221
|
+
async getOperationsBatch(nodeTypes) {
|
|
222
|
+
const result = new Map();
|
|
223
|
+
if (nodeTypes.length === 0)
|
|
224
|
+
return result;
|
|
225
|
+
// Try batch fetch from Redis first
|
|
226
|
+
if (this.isConnected && this.redis) {
|
|
227
|
+
try {
|
|
228
|
+
const keys = nodeTypes.map((nt) => `${PREFIX}operations:${normalizeNodeType(nt)}`);
|
|
229
|
+
const values = await this.redis.mget(...keys);
|
|
230
|
+
const missingTypes = [];
|
|
231
|
+
for (let i = 0; i < nodeTypes.length; i++) {
|
|
232
|
+
const value = values[i];
|
|
233
|
+
const normalizedType = normalizeNodeType(nodeTypes[i]);
|
|
234
|
+
if (value) {
|
|
235
|
+
this.stats.hits++;
|
|
236
|
+
result.set(normalizedType, JSON.parse(value));
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
missingTypes.push(nodeTypes[i]);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Fetch missing from DB in single batch query
|
|
243
|
+
if (missingTypes.length > 0) {
|
|
244
|
+
this.stats.misses += missingTypes.length;
|
|
245
|
+
logger.debug(`[CACHE] Operations cache miss: ${missingTypes.length} node types`);
|
|
246
|
+
// Batch query for all missing operations
|
|
247
|
+
const placeholders = missingTypes.map((_, i) => `$${i + 1}`).join(', ');
|
|
248
|
+
const normalizedTypes = missingTypes.map((nt) => normalizeNodeType(nt));
|
|
249
|
+
const dbResult = await validatorQuery(`SELECT o.id, n.node_type, o.display_name AS operation_name,
|
|
250
|
+
o.operation AS operation_value, o.description, o.required_parameters
|
|
251
|
+
FROM bluelime.operations o
|
|
252
|
+
JOIN bluelime.nodes n ON o.node_id = n.id
|
|
253
|
+
WHERE LOWER(n.node_type) IN (${placeholders})`, normalizedTypes);
|
|
254
|
+
// Group results by node_type
|
|
255
|
+
const byNodeType = new Map();
|
|
256
|
+
for (const op of dbResult.rows) {
|
|
257
|
+
const normalized = normalizeNodeType(op.node_type);
|
|
258
|
+
if (!byNodeType.has(normalized)) {
|
|
259
|
+
byNodeType.set(normalized, []);
|
|
260
|
+
}
|
|
261
|
+
byNodeType.get(normalized).push(op);
|
|
262
|
+
}
|
|
263
|
+
// Cache and add to result
|
|
264
|
+
const pipeline = this.redis.pipeline();
|
|
265
|
+
for (const nt of missingTypes) {
|
|
266
|
+
const normalized = normalizeNodeType(nt);
|
|
267
|
+
const ops = byNodeType.get(normalized) || [];
|
|
268
|
+
result.set(normalized, ops);
|
|
269
|
+
pipeline.setex(`${PREFIX}operations:${normalized}`, this.config.ttlSeconds, JSON.stringify(ops));
|
|
270
|
+
}
|
|
271
|
+
await pipeline.exec();
|
|
272
|
+
}
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
// Fall through to sequential fallback
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// Fallback: sequential fetches
|
|
280
|
+
for (const nodeType of nodeTypes) {
|
|
281
|
+
const ops = await this.getOperations(nodeType);
|
|
282
|
+
result.set(normalizeNodeType(nodeType), ops);
|
|
283
|
+
}
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
async getParameters(operationId) {
|
|
287
|
+
const key = `${PREFIX}parameters:${operationId}`;
|
|
288
|
+
const result = await this.getCachedOrFallback(key, async () => {
|
|
289
|
+
const dbResult = await validatorQuery('SELECT * FROM bluelime.parameters WHERE operation_id = $1', [operationId]);
|
|
290
|
+
return dbResult.rows;
|
|
291
|
+
});
|
|
292
|
+
return result ?? [];
|
|
293
|
+
}
|
|
294
|
+
async getParametersBatch(operationIds) {
|
|
295
|
+
const result = new Map();
|
|
296
|
+
if (operationIds.length === 0)
|
|
297
|
+
return result;
|
|
298
|
+
// Try batch fetch from Redis first
|
|
299
|
+
if (this.isConnected && this.redis) {
|
|
300
|
+
try {
|
|
301
|
+
const keys = operationIds.map((id) => `${PREFIX}parameters:${id}`);
|
|
302
|
+
const values = await this.redis.mget(...keys);
|
|
303
|
+
const missingIds = [];
|
|
304
|
+
for (let i = 0; i < operationIds.length; i++) {
|
|
305
|
+
const value = values[i];
|
|
306
|
+
if (value) {
|
|
307
|
+
this.stats.hits++;
|
|
308
|
+
result.set(operationIds[i], JSON.parse(value));
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
missingIds.push(operationIds[i]);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// RAG-2.2.93.13: Fetch missing from DB in single batch query (not parallel)
|
|
315
|
+
// This prevents overwhelming the connection pool with many parallel queries
|
|
316
|
+
if (missingIds.length > 0) {
|
|
317
|
+
this.stats.misses += missingIds.length;
|
|
318
|
+
logger.debug(`[CACHE] Parameter cache miss: ${missingIds.length} operation IDs`);
|
|
319
|
+
// Single batch query for all missing parameters
|
|
320
|
+
const placeholders = missingIds.map((_, i) => `$${i + 1}`).join(', ');
|
|
321
|
+
const dbResult = await validatorQuery(`SELECT * FROM bluelime.parameters WHERE operation_id IN (${placeholders})`, missingIds);
|
|
322
|
+
// Group results by operation_id
|
|
323
|
+
const byOperationId = new Map();
|
|
324
|
+
for (const param of dbResult.rows) {
|
|
325
|
+
if (!byOperationId.has(param.operation_id)) {
|
|
326
|
+
byOperationId.set(param.operation_id, []);
|
|
327
|
+
}
|
|
328
|
+
byOperationId.get(param.operation_id).push(param);
|
|
329
|
+
}
|
|
330
|
+
// Cache and add to result
|
|
331
|
+
const pipeline = this.redis.pipeline();
|
|
332
|
+
for (const id of missingIds) {
|
|
333
|
+
const params = byOperationId.get(id) || [];
|
|
334
|
+
result.set(id, params);
|
|
335
|
+
pipeline.setex(`${PREFIX}parameters:${id}`, this.config.ttlSeconds, JSON.stringify(params));
|
|
336
|
+
}
|
|
337
|
+
await pipeline.exec();
|
|
338
|
+
}
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
// Fall through to sequential fallback
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// Fallback: sequential fetches
|
|
346
|
+
for (const id of operationIds) {
|
|
347
|
+
const params = await this.getParameters(id);
|
|
348
|
+
result.set(id, params);
|
|
349
|
+
}
|
|
350
|
+
return result;
|
|
351
|
+
}
|
|
352
|
+
async getParametersByOperation(nodeType, operationValue) {
|
|
353
|
+
const operation = await this.getOperation(nodeType, operationValue);
|
|
354
|
+
if (!operation)
|
|
355
|
+
return [];
|
|
356
|
+
return this.getParameters(operation.id);
|
|
357
|
+
}
|
|
358
|
+
// ============================================================
|
|
359
|
+
// Credential Methods
|
|
360
|
+
// ============================================================
|
|
361
|
+
async getCredential(credType) {
|
|
362
|
+
const key = `${PREFIX}credentials:${credType.toLowerCase()}`;
|
|
363
|
+
return this.getCachedOrFallback(key, async () => {
|
|
364
|
+
const result = await validatorQuery('SELECT * FROM bluelime.credentials WHERE LOWER(credential_type) = $1', [credType.toLowerCase()]);
|
|
365
|
+
return result.rows[0] ?? null;
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
async hasCredential(credType) {
|
|
369
|
+
const cred = await this.getCredential(credType);
|
|
370
|
+
return cred !== null;
|
|
371
|
+
}
|
|
372
|
+
async getAllCredentialTypes() {
|
|
373
|
+
return this.getSetMembers(`${PREFIX}credentials:_all`);
|
|
374
|
+
}
|
|
375
|
+
async getCredentialsForNode(nodeType) {
|
|
376
|
+
const key = `${PREFIX}node_credentials:${normalizeNodeType(nodeType)}`;
|
|
377
|
+
return this.getSetMembers(key);
|
|
378
|
+
}
|
|
379
|
+
// ============================================================
|
|
380
|
+
// Connection Methods
|
|
381
|
+
// ============================================================
|
|
382
|
+
async getConnection(source, target) {
|
|
383
|
+
const key = this.getConnectionKey(source, target);
|
|
384
|
+
return this.getCachedOrFallback(key, async () => {
|
|
385
|
+
const result = await validatorQuery(`SELECT source_node_type AS source_type, target_node_type AS target_type,
|
|
386
|
+
occurrence_count, 1.0 AS confidence
|
|
387
|
+
FROM bluelime.connection_rules
|
|
388
|
+
WHERE LOWER(source_node_type) = $1 AND LOWER(target_node_type) = $2`, [normalizeNodeType(source), normalizeNodeType(target)]);
|
|
389
|
+
return result.rows[0] ?? null;
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
async hasConnection(source, target) {
|
|
393
|
+
const conn = await this.getConnection(source, target);
|
|
394
|
+
return conn !== null;
|
|
395
|
+
}
|
|
396
|
+
async getOutgoingConnections(source) {
|
|
397
|
+
const key = `${PREFIX}connections:out:${normalizeNodeType(source)}`;
|
|
398
|
+
const result = await this.getCachedOrFallback(key, async () => {
|
|
399
|
+
const dbResult = await validatorQuery(`SELECT source_node_type AS source_type, target_node_type AS target_type,
|
|
400
|
+
occurrence_count, 1.0 AS confidence
|
|
401
|
+
FROM bluelime.connection_rules WHERE LOWER(source_node_type) = $1`, [normalizeNodeType(source)]);
|
|
402
|
+
return dbResult.rows;
|
|
403
|
+
});
|
|
404
|
+
return result ?? [];
|
|
405
|
+
}
|
|
406
|
+
async getIncomingConnections(target) {
|
|
407
|
+
const key = `${PREFIX}connections:in:${normalizeNodeType(target)}`;
|
|
408
|
+
const result = await this.getCachedOrFallback(key, async () => {
|
|
409
|
+
const dbResult = await validatorQuery(`SELECT source_node_type AS source_type, target_node_type AS target_type,
|
|
410
|
+
occurrence_count, 1.0 AS confidence
|
|
411
|
+
FROM bluelime.connection_rules WHERE LOWER(target_node_type) = $1`, [normalizeNodeType(target)]);
|
|
412
|
+
return dbResult.rows;
|
|
413
|
+
});
|
|
414
|
+
return result ?? [];
|
|
415
|
+
}
|
|
416
|
+
// ============================================================
|
|
417
|
+
// Private: Redis Connection
|
|
418
|
+
// ============================================================
|
|
419
|
+
async connectRedis() {
|
|
420
|
+
const redisUrl = this.config.redisUrl ||
|
|
421
|
+
process.env.REDIS_URL ||
|
|
422
|
+
'redis://localhost:6379';
|
|
423
|
+
try {
|
|
424
|
+
this.redis = new Redis(redisUrl, {
|
|
425
|
+
enableAutoPipelining: true,
|
|
426
|
+
maxRetriesPerRequest: 1,
|
|
427
|
+
connectTimeout: 2000,
|
|
428
|
+
commandTimeout: 5000,
|
|
429
|
+
retryStrategy: (times) => {
|
|
430
|
+
if (times > 2)
|
|
431
|
+
return null;
|
|
432
|
+
return Math.min(times * 100, 1000);
|
|
433
|
+
},
|
|
434
|
+
enableReadyCheck: true,
|
|
435
|
+
});
|
|
436
|
+
await this.waitForConnection();
|
|
437
|
+
await this.redis.ping();
|
|
438
|
+
this.isConnected = true;
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
this.handleConnectionError(error);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
async waitForConnection() {
|
|
445
|
+
return new Promise((resolve, reject) => {
|
|
446
|
+
const timeout = setTimeout(() => {
|
|
447
|
+
reject(new Error('Redis connection timeout'));
|
|
448
|
+
}, 5000);
|
|
449
|
+
this.redis.on('ready', () => {
|
|
450
|
+
clearTimeout(timeout);
|
|
451
|
+
resolve();
|
|
452
|
+
});
|
|
453
|
+
this.redis.on('error', (err) => {
|
|
454
|
+
clearTimeout(timeout);
|
|
455
|
+
reject(err);
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
handleConnectionError(error) {
|
|
460
|
+
logger.warn('Failed to connect to Redis', {
|
|
461
|
+
error: error instanceof Error ? error.message : String(error),
|
|
462
|
+
});
|
|
463
|
+
if (this.redis) {
|
|
464
|
+
this.redis.quit().catch((err) => {
|
|
465
|
+
logger.debug('Redis quit failed during connection error cleanup', { error: err.message });
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
this.redis = null;
|
|
469
|
+
this.isConnected = false;
|
|
470
|
+
}
|
|
471
|
+
// ============================================================
|
|
472
|
+
// Private: Cache Population
|
|
473
|
+
// ============================================================
|
|
474
|
+
async populateCache(data) {
|
|
475
|
+
if (!this.isConnected || !this.redis)
|
|
476
|
+
return;
|
|
477
|
+
const pipeline = this.redis.pipeline();
|
|
478
|
+
const ttl = this.config.ttlSeconds;
|
|
479
|
+
this.populateNodes(pipeline, data.nodes.data, ttl);
|
|
480
|
+
this.populateOperations(pipeline, data.operations.data, ttl);
|
|
481
|
+
this.populateParameters(pipeline, data.parameters.data, ttl);
|
|
482
|
+
this.populateCredentials(pipeline, data.credentials.data, ttl);
|
|
483
|
+
this.populateConnections(pipeline, data.connections.data, ttl);
|
|
484
|
+
await pipeline.exec();
|
|
485
|
+
this.stats.nodes = data.nodes.count;
|
|
486
|
+
this.stats.operations = data.operations.count;
|
|
487
|
+
this.stats.parameters = data.parameters.count;
|
|
488
|
+
this.stats.credentials = data.credentials.count;
|
|
489
|
+
this.stats.connections = data.connections.count;
|
|
490
|
+
}
|
|
491
|
+
populateNodes(pipeline, nodes, ttl) {
|
|
492
|
+
const triggers = [];
|
|
493
|
+
const deprecated = [];
|
|
494
|
+
const roleMap = new Map();
|
|
495
|
+
const allNodes = [];
|
|
496
|
+
for (const node of nodes) {
|
|
497
|
+
const normalizedType = normalizeNodeType(node.node_type);
|
|
498
|
+
const key = `${PREFIX}nodes:${normalizedType}`;
|
|
499
|
+
pipeline.setex(key, ttl, JSON.stringify(node));
|
|
500
|
+
allNodes.push(normalizedType);
|
|
501
|
+
if (node.is_trigger)
|
|
502
|
+
triggers.push(normalizedType);
|
|
503
|
+
if (node.is_deprecated)
|
|
504
|
+
deprecated.push(normalizedType);
|
|
505
|
+
const role = node.node_role || 'unknown';
|
|
506
|
+
if (!roleMap.has(role))
|
|
507
|
+
roleMap.set(role, []);
|
|
508
|
+
roleMap.get(role).push(normalizedType);
|
|
509
|
+
}
|
|
510
|
+
if (allNodes.length > 0) {
|
|
511
|
+
pipeline.del(`${PREFIX}nodes:_all`);
|
|
512
|
+
pipeline.sadd(`${PREFIX}nodes:_all`, ...allNodes);
|
|
513
|
+
}
|
|
514
|
+
if (triggers.length > 0) {
|
|
515
|
+
pipeline.del(`${PREFIX}nodes:_triggers`);
|
|
516
|
+
pipeline.sadd(`${PREFIX}nodes:_triggers`, ...triggers);
|
|
517
|
+
}
|
|
518
|
+
if (deprecated.length > 0) {
|
|
519
|
+
pipeline.del(`${PREFIX}nodes:_deprecated`);
|
|
520
|
+
pipeline.sadd(`${PREFIX}nodes:_deprecated`, ...deprecated);
|
|
521
|
+
}
|
|
522
|
+
Array.from(roleMap.entries()).forEach(([role, types]) => {
|
|
523
|
+
const roleKey = `${PREFIX}nodes:_roles:${role}`;
|
|
524
|
+
pipeline.del(roleKey);
|
|
525
|
+
pipeline.sadd(roleKey, ...types);
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
populateOperations(pipeline, operations, ttl) {
|
|
529
|
+
const byNode = new Map();
|
|
530
|
+
for (const op of operations) {
|
|
531
|
+
const normalized = normalizeNodeType(op.node_type);
|
|
532
|
+
if (!byNode.has(normalized))
|
|
533
|
+
byNode.set(normalized, []);
|
|
534
|
+
byNode.get(normalized).push(op);
|
|
535
|
+
}
|
|
536
|
+
Array.from(byNode.entries()).forEach(([nodeType, ops]) => {
|
|
537
|
+
const key = `${PREFIX}operations:${nodeType}`;
|
|
538
|
+
pipeline.setex(key, ttl, JSON.stringify(ops));
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
populateParameters(pipeline, parameters, ttl) {
|
|
542
|
+
const byOperation = new Map();
|
|
543
|
+
for (const param of parameters) {
|
|
544
|
+
if (!byOperation.has(param.operation_id)) {
|
|
545
|
+
byOperation.set(param.operation_id, []);
|
|
546
|
+
}
|
|
547
|
+
byOperation.get(param.operation_id).push(param);
|
|
548
|
+
}
|
|
549
|
+
Array.from(byOperation.entries()).forEach(([opId, params]) => {
|
|
550
|
+
const key = `${PREFIX}parameters:${opId}`;
|
|
551
|
+
pipeline.setex(key, ttl, JSON.stringify(params));
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
populateCredentials(pipeline, credentials, ttl) {
|
|
555
|
+
const allCreds = [];
|
|
556
|
+
for (const cred of credentials) {
|
|
557
|
+
const normalized = cred.credential_type.toLowerCase();
|
|
558
|
+
const key = `${PREFIX}credentials:${normalized}`;
|
|
559
|
+
pipeline.setex(key, ttl, JSON.stringify(cred));
|
|
560
|
+
allCreds.push(normalized);
|
|
561
|
+
}
|
|
562
|
+
if (allCreds.length > 0) {
|
|
563
|
+
pipeline.del(`${PREFIX}credentials:_all`);
|
|
564
|
+
pipeline.sadd(`${PREFIX}credentials:_all`, ...allCreds);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
populateConnections(pipeline, connections, ttl) {
|
|
568
|
+
const outgoing = new Map();
|
|
569
|
+
const incoming = new Map();
|
|
570
|
+
for (const conn of connections) {
|
|
571
|
+
const key = this.getConnectionKey(conn.source_type, conn.target_type);
|
|
572
|
+
pipeline.setex(key, ttl, JSON.stringify(conn));
|
|
573
|
+
const srcNorm = normalizeNodeType(conn.source_type);
|
|
574
|
+
const tgtNorm = normalizeNodeType(conn.target_type);
|
|
575
|
+
if (!outgoing.has(srcNorm))
|
|
576
|
+
outgoing.set(srcNorm, []);
|
|
577
|
+
outgoing.get(srcNorm).push(conn);
|
|
578
|
+
if (!incoming.has(tgtNorm))
|
|
579
|
+
incoming.set(tgtNorm, []);
|
|
580
|
+
incoming.get(tgtNorm).push(conn);
|
|
581
|
+
}
|
|
582
|
+
Array.from(outgoing.entries()).forEach(([src, conns]) => {
|
|
583
|
+
const key = `${PREFIX}connections:out:${src}`;
|
|
584
|
+
pipeline.setex(key, ttl, JSON.stringify(conns));
|
|
585
|
+
});
|
|
586
|
+
Array.from(incoming.entries()).forEach(([tgt, conns]) => {
|
|
587
|
+
const key = `${PREFIX}connections:in:${tgt}`;
|
|
588
|
+
pipeline.setex(key, ttl, JSON.stringify(conns));
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
// ============================================================
|
|
592
|
+
// Private: Cache Helpers
|
|
593
|
+
// ============================================================
|
|
594
|
+
async getCachedOrFallback(key, fallback) {
|
|
595
|
+
if (this.isConnected && this.redis) {
|
|
596
|
+
try {
|
|
597
|
+
const cached = await this.redis.get(key);
|
|
598
|
+
if (cached) {
|
|
599
|
+
this.stats.hits++;
|
|
600
|
+
return JSON.parse(cached);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
// Fall through to DB
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
this.stats.misses++;
|
|
608
|
+
const result = await fallback();
|
|
609
|
+
if (result && this.isConnected && this.redis) {
|
|
610
|
+
await this.redis
|
|
611
|
+
.setex(key, this.config.ttlSeconds, JSON.stringify(result))
|
|
612
|
+
.catch((err) => {
|
|
613
|
+
logger.debug('Redis setex failed (non-critical)', { key, error: err.message });
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
return result;
|
|
617
|
+
}
|
|
618
|
+
async getSetMembers(key) {
|
|
619
|
+
if (this.isConnected && this.redis) {
|
|
620
|
+
try {
|
|
621
|
+
return await this.redis.smembers(key);
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
return [];
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return [];
|
|
628
|
+
}
|
|
629
|
+
getConnectionKey(source, target) {
|
|
630
|
+
return `${PREFIX}connections:${normalizeNodeType(source)}:${normalizeNodeType(target)}`;
|
|
631
|
+
}
|
|
632
|
+
getBackend() {
|
|
633
|
+
if (this.isConnected && this.redis)
|
|
634
|
+
return 'redis';
|
|
635
|
+
return 'none';
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
/** Singleton instance */
|
|
639
|
+
let instance = null;
|
|
640
|
+
/** Promise singleton for initialization (prevents race conditions) */
|
|
641
|
+
let initPromise = null;
|
|
642
|
+
/**
|
|
643
|
+
* Get or create the singleton RedisCatalogCache instance.
|
|
644
|
+
*/
|
|
645
|
+
export function getRedisCatalogCache(config) {
|
|
646
|
+
if (!instance) {
|
|
647
|
+
instance = new RedisCatalogCache(config);
|
|
648
|
+
}
|
|
649
|
+
return instance;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Initialize and return the singleton cache.
|
|
653
|
+
* Convenience function that creates, initializes, and returns the cache.
|
|
654
|
+
* Uses promise singleton pattern to prevent race conditions when called concurrently.
|
|
655
|
+
*/
|
|
656
|
+
export function initRedisCatalogCache(config) {
|
|
657
|
+
if (!initPromise) {
|
|
658
|
+
initPromise = (async () => {
|
|
659
|
+
const cache = getRedisCatalogCache(config);
|
|
660
|
+
if (!cache.isReady()) {
|
|
661
|
+
await cache.initialize();
|
|
662
|
+
}
|
|
663
|
+
return cache;
|
|
664
|
+
})();
|
|
665
|
+
}
|
|
666
|
+
return initPromise;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Reset the singleton instance (for testing).
|
|
670
|
+
*/
|
|
671
|
+
export function resetRedisCatalogCache() {
|
|
672
|
+
if (instance) {
|
|
673
|
+
instance.dispose().catch((err) => {
|
|
674
|
+
logger.debug('Dispose failed during reset', { error: err.message });
|
|
675
|
+
});
|
|
676
|
+
instance = null;
|
|
677
|
+
}
|
|
678
|
+
initPromise = null;
|
|
679
|
+
}
|
|
680
|
+
//# sourceMappingURL=RedisCatalogCache.js.map
|