"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ResourceSimilarityService = void 0;
const logger_1 = require("../utils/logger");
class ResourceSimilarityService {
constructor(repository) {
this.resourceCache = new Map();
this.suggestionCache = new Map();
this.repository = repository;
this.commonPatterns = this.initializeCommonPatterns();
}
cleanupExpiredEntries() {
const now = Date.now();
for (const [key, value] of this.resourceCache.entries()) {
if (now - value.timestamp >= ResourceSimilarityService.CACHE_DURATION_MS) {
this.resourceCache.delete(key);
}
}
if (this.suggestionCache.size > 100) {
const entries = Array.from(this.suggestionCache.entries());
this.suggestionCache.clear();
entries.slice(-50).forEach(([key, value]) => {
this.suggestionCache.set(key, value);
});
}
}
initializeCommonPatterns() {
const patterns = new Map();
patterns.set('googleDrive', [
{ pattern: 'files', suggestion: 'file', confidence: 0.95, reason: 'Use singular "file" not plural' },
{ pattern: 'folders', suggestion: 'folder', confidence: 0.95, reason: 'Use singular "folder" not plural' },
{ pattern: 'permissions', suggestion: 'permission', confidence: 0.9, reason: 'Use singular form' },
{ pattern: 'fileAndFolder', suggestion: 'fileFolder', confidence: 0.9, reason: 'Use "fileFolder" for combined operations' },
{ pattern: 'driveFiles', suggestion: 'file', confidence: 0.8, reason: 'Use "file" for file operations' },
{ pattern: 'sharedDrives', suggestion: 'drive', confidence: 0.85, reason: 'Use "drive" for shared drive operations' },
]);
patterns.set('slack', [
{ pattern: 'messages', suggestion: 'message', confidence: 0.95, reason: 'Use singular "message" not plural' },
{ pattern: 'channels', suggestion: 'channel', confidence: 0.95, reason: 'Use singular "channel" not plural' },
{ pattern: 'users', suggestion: 'user', confidence: 0.95, reason: 'Use singular "user" not plural' },
{ pattern: 'msg', suggestion: 'message', confidence: 0.85, reason: 'Use full "message" not abbreviation' },
{ pattern: 'dm', suggestion: 'message', confidence: 0.7, reason: 'Use "message" for direct messages' },
{ pattern: 'conversation', suggestion: 'channel', confidence: 0.7, reason: 'Use "channel" for conversations' },
]);
patterns.set('database', [
{ pattern: 'tables', suggestion: 'table', confidence: 0.95, reason: 'Use singular "table" not plural' },
{ pattern: 'queries', suggestion: 'query', confidence: 0.95, reason: 'Use singular "query" not plural' },
{ pattern: 'collections', suggestion: 'collection', confidence: 0.95, reason: 'Use singular "collection" not plural' },
{ pattern: 'documents', suggestion: 'document', confidence: 0.95, reason: 'Use singular "document" not plural' },
{ pattern: 'records', suggestion: 'record', confidence: 0.85, reason: 'Use "record" or "document"' },
{ pattern: 'rows', suggestion: 'row', confidence: 0.9, reason: 'Use singular "row"' },
]);
patterns.set('googleSheets', [
{ pattern: 'sheets', suggestion: 'sheet', confidence: 0.95, reason: 'Use singular "sheet" not plural' },
{ pattern: 'spreadsheets', suggestion: 'spreadsheet', confidence: 0.95, reason: 'Use singular "spreadsheet"' },
{ pattern: 'cells', suggestion: 'cell', confidence: 0.9, reason: 'Use singular "cell"' },
{ pattern: 'ranges', suggestion: 'range', confidence: 0.9, reason: 'Use singular "range"' },
{ pattern: 'worksheets', suggestion: 'sheet', confidence: 0.8, reason: 'Use "sheet" for worksheet operations' },
]);
patterns.set('email', [
{ pattern: 'emails', suggestion: 'email', confidence: 0.95, reason: 'Use singular "email" not plural' },
{ pattern: 'messages', suggestion: 'message', confidence: 0.9, reason: 'Use "message" for email operations' },
{ pattern: 'mails', suggestion: 'email', confidence: 0.9, reason: 'Use "email" not "mail"' },
{ pattern: 'attachments', suggestion: 'attachment', confidence: 0.95, reason: 'Use singular "attachment"' },
]);
patterns.set('generic', [
{ pattern: 'items', suggestion: 'item', confidence: 0.9, reason: 'Use singular form' },
{ pattern: 'objects', suggestion: 'object', confidence: 0.9, reason: 'Use singular form' },
{ pattern: 'entities', suggestion: 'entity', confidence: 0.9, reason: 'Use singular form' },
{ pattern: 'resources', suggestion: 'resource', confidence: 0.9, reason: 'Use singular form' },
{ pattern: 'elements', suggestion: 'element', confidence: 0.9, reason: 'Use singular form' },
]);
return patterns;
}
findSimilarResources(nodeType, invalidResource, maxSuggestions = ResourceSimilarityService.MAX_SUGGESTIONS) {
if (Math.random() < 0.1) {
this.cleanupExpiredEntries();
}
const cacheKey = `${nodeType}:${invalidResource}`;
if (this.suggestionCache.has(cacheKey)) {
return this.suggestionCache.get(cacheKey);
}
const suggestions = [];
const validResources = this.getNodeResources(nodeType);
for (const resource of validResources) {
const resourceValue = this.getResourceValue(resource);
if (resourceValue.toLowerCase() === invalidResource.toLowerCase()) {
return [];
}
}
const nodePatterns = this.getNodePatterns(nodeType);
for (const pattern of nodePatterns) {
if (pattern.pattern.toLowerCase() === invalidResource.toLowerCase()) {
const exists = validResources.some(r => {
const resourceValue = this.getResourceValue(r);
return resourceValue === pattern.suggestion;
});
if (exists) {
suggestions.push({
value: pattern.suggestion,
confidence: pattern.confidence,
reason: pattern.reason
});
}
}
}
const singularForm = this.toSingular(invalidResource);
const pluralForm = this.toPlural(invalidResource);
for (const resource of validResources) {
const resourceValue = this.getResourceValue(resource);
if (resourceValue === singularForm || resourceValue === pluralForm) {
if (!suggestions.some(s => s.value === resourceValue)) {
suggestions.push({
value: resourceValue,
confidence: 0.9,
reason: invalidResource.endsWith('s') ?
'Use singular form for resources' :
'Incorrect plural/singular form',
availableOperations: typeof resource === 'object' ? resource.operations : undefined
});
}
}
const similarity = this.calculateSimilarity(invalidResource, resourceValue);
if (similarity >= ResourceSimilarityService.MIN_CONFIDENCE) {
if (!suggestions.some(s => s.value === resourceValue)) {
suggestions.push({
value: resourceValue,
confidence: similarity,
reason: this.getSimilarityReason(similarity, invalidResource, resourceValue),
availableOperations: typeof resource === 'object' ? resource.operations : undefined
});
}
}
}
suggestions.sort((a, b) => b.confidence - a.confidence);
const topSuggestions = suggestions.slice(0, maxSuggestions);
this.suggestionCache.set(cacheKey, topSuggestions);
return topSuggestions;
}
getResourceValue(resource) {
if (typeof resource === 'string') {
return resource;
}
if (typeof resource === 'object' && resource !== null) {
return resource.value || '';
}
return '';
}
getNodeResources(nodeType) {
if (Math.random() < 0.05) {
this.cleanupExpiredEntries();
}
const cacheKey = nodeType;
const cached = this.resourceCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < ResourceSimilarityService.CACHE_DURATION_MS) {
return cached.resources;
}
const nodeInfo = this.repository.getNode(nodeType);
if (!nodeInfo)
return [];
const resources = [];
const resourceMap = new Map();
try {
const properties = nodeInfo.properties || [];
for (const prop of properties) {
if (prop.name === 'resource' && prop.options) {
for (const option of prop.options) {
resources.push({
value: option.value,
name: option.name,
operations: []
});
resourceMap.set(option.value, []);
}
}
if (prop.name === 'operation' && prop.displayOptions?.show?.resource) {
const resourceValues = Array.isArray(prop.displayOptions.show.resource)
? prop.displayOptions.show.resource
: [prop.displayOptions.show.resource];
for (const resourceValue of resourceValues) {
if (resourceMap.has(resourceValue) && prop.options) {
const ops = prop.options.map((op) => op.value);
resourceMap.get(resourceValue).push(...ops);
}
}
}
}
for (const resource of resources) {
if (resourceMap.has(resource.value)) {
resource.operations = resourceMap.get(resource.value);
}
}
if (resources.length === 0) {
const implicitResources = this.extractImplicitResources(properties);
resources.push(...implicitResources);
}
}
catch (error) {
logger_1.logger.warn(`Failed to extract resources for ${nodeType}:`, error);
}
this.resourceCache.set(cacheKey, { resources, timestamp: Date.now() });
return resources;
}
extractImplicitResources(properties) {
const resources = [];
for (const prop of properties) {
if (prop.name === 'operation' && prop.options) {
const resourceFromOps = this.inferResourceFromOperations(prop.options);
if (resourceFromOps) {
resources.push({
value: resourceFromOps,
name: resourceFromOps.charAt(0).toUpperCase() + resourceFromOps.slice(1),
operations: prop.options.map((op) => op.value)
});
}
}
}
return resources;
}
inferResourceFromOperations(operations) {
const patterns = [
{ keywords: ['file', 'upload', 'download'], resource: 'file' },
{ keywords: ['folder', 'directory'], resource: 'folder' },
{ keywords: ['message', 'send', 'reply'], resource: 'message' },
{ keywords: ['channel', 'broadcast'], resource: 'channel' },
{ keywords: ['user', 'member'], resource: 'user' },
{ keywords: ['table', 'row', 'column'], resource: 'table' },
{ keywords: ['document', 'doc'], resource: 'document' },
];
for (const pattern of patterns) {
for (const op of operations) {
const opName = (op.value || op).toLowerCase();
if (pattern.keywords.some(keyword => opName.includes(keyword))) {
return pattern.resource;
}
}
}
return null;
}
getNodePatterns(nodeType) {
const patterns = [];
if (nodeType.includes('googleDrive')) {
patterns.push(...(this.commonPatterns.get('googleDrive') || []));
}
else if (nodeType.includes('slack')) {
patterns.push(...(this.commonPatterns.get('slack') || []));
}
else if (nodeType.includes('postgres') || nodeType.includes('mysql') || nodeType.includes('mongodb')) {
patterns.push(...(this.commonPatterns.get('database') || []));
}
else if (nodeType.includes('googleSheets')) {
patterns.push(...(this.commonPatterns.get('googleSheets') || []));
}
else if (nodeType.includes('gmail') || nodeType.includes('email')) {
patterns.push(...(this.commonPatterns.get('email') || []));
}
patterns.push(...(this.commonPatterns.get('generic') || []));
return patterns;
}
toSingular(word) {
if (word.endsWith('ies')) {
return word.slice(0, -3) + 'y';
}
else if (word.endsWith('es')) {
return word.slice(0, -2);
}
else if (word.endsWith('s') && !word.endsWith('ss')) {
return word.slice(0, -1);
}
return word;
}
toPlural(word) {
if (word.endsWith('y') && !['ay', 'ey', 'iy', 'oy', 'uy'].includes(word.slice(-2))) {
return word.slice(0, -1) + 'ies';
}
else if (word.endsWith('s') || word.endsWith('x') || word.endsWith('z') ||
word.endsWith('ch') || word.endsWith('sh')) {
return word + 'es';
}
else {
return word + 's';
}
}
calculateSimilarity(str1, str2) {
const s1 = str1.toLowerCase();
const s2 = str2.toLowerCase();
if (s1 === s2)
return 1.0;
if (s1.includes(s2) || s2.includes(s1)) {
const ratio = Math.min(s1.length, s2.length) / Math.max(s1.length, s2.length);
return Math.max(ResourceSimilarityService.CONFIDENCE_THRESHOLDS.MIN_SUBSTRING, ratio);
}
const distance = this.levenshteinDistance(s1, s2);
const maxLength = Math.max(s1.length, s2.length);
let similarity = 1 - (distance / maxLength);
if (distance === 1 && maxLength <= 5) {
similarity = Math.max(similarity, 0.75);
}
else if (distance === 2 && maxLength <= 5) {
similarity = Math.max(similarity, 0.72);
}
return similarity;
}
levenshteinDistance(str1, str2) {
const m = str1.length;
const n = str2.length;
const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
for (let i = 0; i <= m; i++)
dp[i][0] = i;
for (let j = 0; j <= n; j++)
dp[0][j] = j;
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (str1[i - 1] === str2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
}
else {
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1);
}
}
}
return dp[m][n];
}
getSimilarityReason(confidence, invalid, valid) {
const { VERY_HIGH, HIGH, MEDIUM } = ResourceSimilarityService.CONFIDENCE_THRESHOLDS;
if (confidence >= VERY_HIGH) {
return 'Almost exact match - likely a typo';
}
else if (confidence >= HIGH) {
return 'Very similar - common variation';
}
else if (confidence >= MEDIUM) {
return 'Similar resource name';
}
else if (invalid.includes(valid) || valid.includes(invalid)) {
return 'Partial match';
}
else {
return 'Possibly related resource';
}
}
clearCache() {
this.resourceCache.clear();
this.suggestionCache.clear();
}
}
exports.ResourceSimilarityService = ResourceSimilarityService;
ResourceSimilarityService.CACHE_DURATION_MS = 5 * 60 * 1000;
ResourceSimilarityService.MIN_CONFIDENCE = 0.3;
ResourceSimilarityService.MAX_SUGGESTIONS = 5;
ResourceSimilarityService.CONFIDENCE_THRESHOLDS = {
EXACT: 1.0,
VERY_HIGH: 0.95,
HIGH: 0.8,
MEDIUM: 0.6,
MIN_SUBSTRING: 0.7
};
//# sourceMappingURL=resource-similarity-service.js.map