diff --git a/gitnexus-shared/src/graph/types.ts b/gitnexus-shared/src/graph/types.ts index 49762d145..26a91f5a4 100644 --- a/gitnexus-shared/src/graph/types.ts +++ b/gitnexus-shared/src/graph/types.ts @@ -44,7 +44,8 @@ export type NodeLabel = | 'Template' | 'Section' | 'Route' - | 'Tool'; + | 'Tool' + | 'Webhook'; export type NodeProperties = { name: string; @@ -89,6 +90,9 @@ export type NodeProperties = { responseKeys?: string[]; errorKeys?: string[]; middleware?: string[]; + // Webhook/event handler + kind?: string; + eventTypes?: string[]; // Extensible [key: string]: unknown; }; @@ -115,7 +119,10 @@ export type RelationshipType = | 'HANDLES_TOOL' | 'ENTRY_POINT_OF' | 'WRAPS' - | 'QUERIES'; + | 'QUERIES' + | 'TRIGGERS' + | 'ENQUEUES' + | 'PROCESSES'; export interface GraphNode { id: string; diff --git a/gitnexus-shared/src/lbug/schema-constants.ts b/gitnexus-shared/src/lbug/schema-constants.ts index 656ffe552..066ac3f30 100644 --- a/gitnexus-shared/src/lbug/schema-constants.ts +++ b/gitnexus-shared/src/lbug/schema-constants.ts @@ -40,6 +40,7 @@ export const NODE_TABLES = [ 'Module', 'Route', 'Tool', + 'Webhook', ] as const; export type NodeTableName = (typeof NODE_TABLES)[number]; @@ -67,6 +68,9 @@ export const REL_TYPES = [ 'ENTRY_POINT_OF', 'WRAPS', 'QUERIES', + 'TRIGGERS', + 'ENQUEUES', + 'PROCESSES', ] as const; export type RelType = (typeof REL_TYPES)[number]; diff --git a/gitnexus/src/core/ingestion/parsing-processor.ts b/gitnexus/src/core/ingestion/parsing-processor.ts index 90186c659..3ee846562 100644 --- a/gitnexus/src/core/ingestion/parsing-processor.ts +++ b/gitnexus/src/core/ingestion/parsing-processor.ts @@ -42,6 +42,7 @@ import type { ExtractedFetchCall, ExtractedDecoratorRoute, ExtractedToolDef, + ExtractedWebhook, FileConstructorBindings, FileScopeBindings, ExtractedORMQuery, @@ -60,6 +61,7 @@ export interface WorkerExtractedData { decoratorRoutes: ExtractedDecoratorRoute[]; toolDefs: ExtractedToolDef[]; ormQueries: ExtractedORMQuery[]; + webhooks: ExtractedWebhook[]; constructorBindings: FileConstructorBindings[]; fileScopeBindings: FileScopeBindings[]; } @@ -94,6 +96,7 @@ const processParsingWithWorkers = async ( decoratorRoutes: [], toolDefs: [], ormQueries: [], + webhooks: [], constructorBindings: [], fileScopeBindings: [], }; @@ -118,6 +121,7 @@ const processParsingWithWorkers = async ( const allDecoratorRoutes: ExtractedDecoratorRoute[] = []; const allToolDefs: ExtractedToolDef[] = []; const allORMQueries: ExtractedORMQuery[] = []; + const allWebhooks: ExtractedWebhook[] = []; const allConstructorBindings: FileConstructorBindings[] = []; const fileScopeBindingsByFile: FileScopeBindings[] = []; for (const result of chunkResults) { @@ -145,18 +149,18 @@ const processParsingWithWorkers = async ( }); } - for (const item of result.imports) allImports.push(item); - for (const item of result.calls) allCalls.push(item); - for (const item of result.assignments) allAssignments.push(item); - for (const item of result.heritage) allHeritage.push(item); - for (const item of result.routes) allRoutes.push(item); - for (const item of result.fetchCalls) allFetchCalls.push(item); - for (const item of result.decoratorRoutes) allDecoratorRoutes.push(item); - for (const item of result.toolDefs) allToolDefs.push(item); - if (result.ormQueries) for (const item of result.ormQueries) allORMQueries.push(item); - for (const item of result.constructorBindings) allConstructorBindings.push(item); - if (result.fileScopeBindings) - for (const item of result.fileScopeBindings) fileScopeBindingsByFile.push(item); + allImports.push(...result.imports); + allCalls.push(...result.calls); + allAssignments.push(...result.assignments); + allHeritage.push(...result.heritage); + allRoutes.push(...result.routes); + allFetchCalls.push(...result.fetchCalls); + allDecoratorRoutes.push(...result.decoratorRoutes); + allToolDefs.push(...result.toolDefs); + if (result.ormQueries) allORMQueries.push(...result.ormQueries); + allWebhooks.push(...result.webhooks); + allConstructorBindings.push(...result.constructorBindings); + if (result.fileScopeBindings) fileScopeBindingsByFile.push(...result.fileScopeBindings); } // Merge and log skipped languages from workers @@ -185,6 +189,7 @@ const processParsingWithWorkers = async ( decoratorRoutes: allDecoratorRoutes, toolDefs: allToolDefs, ormQueries: allORMQueries, + webhooks: allWebhooks, constructorBindings: allConstructorBindings, fileScopeBindings: fileScopeBindingsByFile, }; diff --git a/gitnexus/src/core/ingestion/pipeline.ts b/gitnexus/src/core/ingestion/pipeline.ts index e1f226289..fc04dc3c9 100644 --- a/gitnexus/src/core/ingestion/pipeline.ts +++ b/gitnexus/src/core/ingestion/pipeline.ts @@ -16,7 +16,7 @@ */ import { createKnowledgeGraph } from '../graph/graph.js'; -import { type PipelineProgress } from 'gitnexus-shared'; +import { type PipelineProgress, getLanguageFromFilename } from 'gitnexus-shared'; import { PipelineResult } from '../../types/pipeline.js'; import { runPipeline, diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index 203e027fa..f93bc10cb 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -217,6 +217,23 @@ export interface ExtractedORMQuery { lineNumber: number; } +export interface ExtractedWebhook { + filePath: string; + name: string; + kind: 'stripe' | 'bullmq-consumer' | 'bullmq-producer' | 'edge-function' | 'realtime' | 'generic'; + eventTypes: string[]; + lineNumber: number; +} + +export interface ExtractedQueuePattern { + filePath: string; + role: 'producer' | 'consumer'; + queueName: string; + method?: string; + handlerName?: string; + lineNumber: number; +} + /** Constructor bindings keyed by filePath for cross-file type resolution */ export interface FileConstructorBindings { filePath: string; @@ -266,6 +283,8 @@ export interface ParseWorkerResult { decoratorRoutes: ExtractedDecoratorRoute[]; toolDefs: ExtractedToolDef[]; ormQueries: ExtractedORMQuery[]; + webhooks: ExtractedWebhook[]; + queuePatterns: ExtractedQueuePattern[]; constructorBindings: FileConstructorBindings[]; /** All-scope type bindings from TypeEnv for BindingAccumulator (includes function-local). */ fileScopeBindings: FileScopeBindings[]; @@ -709,6 +728,7 @@ const processBatch = ( decoratorRoutes: [], toolDefs: [], ormQueries: [], + webhooks: [], constructorBindings: [], fileScopeBindings: [], skippedLanguages: {}, @@ -1332,6 +1352,48 @@ export function extractORMQueries( } } +// ============================================================================ +// Webhook/Event Handler Extraction +// ============================================================================ + +function extractWebhooks(filePath: string, content: string, out: ExtractedWebhook[]): void { + if (content.includes('constructEvent') || content.includes('webhooks.constructEvent')) { + const eventTypes: string[] = []; + const caseRe = /case\s+['"]([.\w-]+)['"]/g; + let m; + while ((m = caseRe.exec(content)) !== null) { eventTypes.push(m[1]); } + const idx = content.indexOf('constructEvent'); + out.push({ filePath, name: 'stripe-webhook', kind: 'stripe', eventTypes, + lineNumber: idx > -1 ? content.substring(0, idx).split('\n').length - 1 : 0 }); + return; + } + const edgeFnMatch = filePath.match(/supabase\/functions\/([\w-]+)\/index\.ts$/); + if (edgeFnMatch && content.includes('Deno.serve')) { + const idx = content.indexOf('Deno.serve'); + out.push({ filePath, name: edgeFnMatch[1], kind: 'edge-function', eventTypes: [], + lineNumber: content.substring(0, idx).split('\n').length - 1 }); + return; + } + if (content.includes('postgres_changes')) { + const channelRe = /\.channel\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g; + let m; + while ((m = channelRe.exec(content)) !== null) { + out.push({ filePath, name: `realtime:${m[1]}`, kind: 'realtime', eventTypes: [], + lineNumber: content.substring(0, m.index).split('\n').length - 1 }); + } + } + const lowerPath = filePath.toLowerCase(); + if (lowerPath.includes('webhook') || lowerPath.includes('hook')) { + const hasSigVerify = content.includes('x-webhook-signature') || content.includes('x-hub-signature') + || content.includes('verify'); + if (hasSigVerify) { + const pathParts = filePath.split('/'); + const name = pathParts[pathParts.length - 1].replace(/\.(ts|js|tsx|jsx)$/, ''); + out.push({ filePath, name: `webhook:${name}`, kind: 'generic', eventTypes: [], lineNumber: 0 }); + } + } +} + const processFileGroup = ( files: ParseWorkerInput[], language: SupportedLanguages, @@ -2246,6 +2308,8 @@ const processFileGroup = ( // Extract ORM queries (Prisma, Supabase) extractORMQueries(file.path, parseContent, result.ormQueries); + extractWebhooks(file.path, file.content, result.webhooks); + extractQueuePatterns(file.path, file.content, result.queuePatterns); // Vue: emit CALLS edges for components used in