diff --git a/gitnexus/src/core/graph/types.ts b/gitnexus/src/core/graph/types.ts index e82729fff5..594a94c7e4 100644 --- a/gitnexus/src/core/graph/types.ts +++ b/gitnexus/src/core/graph/types.ts @@ -100,6 +100,7 @@ export type RelationshipType = | 'HANDLES_TOOL' // Function/File → Tool (handler implements this tool) | 'ENTRY_POINT_OF' // Route/Tool → Process (this endpoint starts this execution flow) | 'WRAPS' // Function → Function (middleware wrapper chain) — Reserved: future middleware graph traversal (not yet emitted) + | 'QUERIES' // File/Function → CodeElement (ORM query to model/table) export interface GraphNode { id: string, diff --git a/gitnexus/src/core/ingestion/framework-detection.ts b/gitnexus/src/core/ingestion/framework-detection.ts index 0eb8fca942..c6d0350c71 100644 --- a/gitnexus/src/core/ingestion/framework-detection.ts +++ b/gitnexus/src/core/ingestion/framework-detection.ts @@ -81,6 +81,17 @@ export function detectFrameworkFromPath(filePath: string): FrameworkHint | null return { framework: 'expo-router', entryPointMultiplier: 2.5, reason: 'expo-screen' }; } + // Prisma schema (ORM data model definition) + if (p.includes('/prisma/') && p.endsWith('schema.prisma')) { + return { framework: 'prisma', entryPointMultiplier: 1.5, reason: 'prisma-schema' }; + } + + // Supabase client files + if ((p.includes('/lib/supabase') || p.includes('/utils/supabase') || p.includes('/supabase/')) && + (p.endsWith('.ts') || p.endsWith('.js'))) { + return { framework: 'supabase', entryPointMultiplier: 1.5, reason: 'supabase-client' }; + } + // Express / Node.js routes if (p.includes('/routes/') && (p.endsWith('.ts') || p.endsWith('.js'))) { return { framework: 'express', entryPointMultiplier: 2.5, reason: 'routes-folder' }; @@ -456,6 +467,10 @@ export const FRAMEWORK_AST_PATTERNS = { 'fiber': ['fiber.Ctx', 'fiber.New', 'fiber.App'], 'go-grpc': ['grpc.Server', 'RegisterServer', 'pb.Unimplemented'], + // ORM patterns + 'prisma': ['prisma.', 'PrismaClient', '@prisma/client'], + 'supabase': ['supabase.from', 'createClient', '@supabase/supabase-js'], + // PHP/Laravel 'laravel': ['Route::get', 'Route::post', 'Route::put', 'Route::delete', 'Route::resource', 'Route::apiResource', '#[Route('], diff --git a/gitnexus/src/core/ingestion/parsing-processor.ts b/gitnexus/src/core/ingestion/parsing-processor.ts index d815f5bb49..6734aa7f89 100644 --- a/gitnexus/src/core/ingestion/parsing-processor.ts +++ b/gitnexus/src/core/ingestion/parsing-processor.ts @@ -11,7 +11,7 @@ import { getDefinitionNodeFromCaptures, findEnclosingClassId, extractMethodSigna import { extractPropertyDeclaredType } from './type-extractors/shared.js'; import { detectFrameworkFromAST } from './framework-detection.js'; import { WorkerPool } from './workers/worker-pool.js'; -import type { ParseWorkerResult, ParseWorkerInput, ExtractedImport, ExtractedCall, ExtractedAssignment, ExtractedHeritage, ExtractedRoute, ExtractedFetchCall, ExtractedDecoratorRoute, ExtractedToolDef, FileConstructorBindings, FileTypeEnvBindings } from './workers/parse-worker.js'; +import type { ParseWorkerResult, ParseWorkerInput, ExtractedImport, ExtractedCall, ExtractedAssignment, ExtractedHeritage, ExtractedRoute, ExtractedFetchCall, ExtractedDecoratorRoute, ExtractedToolDef, FileConstructorBindings, FileTypeEnvBindings, ExtractedORMQuery } from './workers/parse-worker.js'; import { getTreeSitterBufferSize, TREE_SITTER_MAX_BUFFER } from './constants.js'; export type FileProgressCallback = (current: number, total: number, filePath: string) => void; @@ -25,6 +25,7 @@ export interface WorkerExtractedData { fetchCalls: ExtractedFetchCall[]; decoratorRoutes: ExtractedDecoratorRoute[]; toolDefs: ExtractedToolDef[]; + ormQueries: ExtractedORMQuery[]; constructorBindings: FileConstructorBindings[]; typeEnvBindings: FileTypeEnvBindings[]; } @@ -48,7 +49,7 @@ const processParsingWithWorkers = async ( if (lang) parseableFiles.push({ path: file.path, content: file.content }); } - if (parseableFiles.length === 0) return { imports: [], calls: [], assignments: [], heritage: [], routes: [], fetchCalls: [], decoratorRoutes: [], toolDefs: [], constructorBindings: [], typeEnvBindings: [] }; + if (parseableFiles.length === 0) return { imports: [], calls: [], assignments: [], heritage: [], routes: [], fetchCalls: [], decoratorRoutes: [], toolDefs: [], ormQueries: [], constructorBindings: [], typeEnvBindings: [] }; const total = files.length; @@ -69,6 +70,7 @@ const processParsingWithWorkers = async ( const allFetchCalls: ExtractedFetchCall[] = []; const allDecoratorRoutes: ExtractedDecoratorRoute[] = []; const allToolDefs: ExtractedToolDef[] = []; + const allORMQueries: ExtractedORMQuery[] = []; const allConstructorBindings: FileConstructorBindings[] = []; const allTypeEnvBindings: FileTypeEnvBindings[] = []; for (const result of chunkResults) { @@ -103,6 +105,7 @@ const processParsingWithWorkers = async ( allFetchCalls.push(...result.fetchCalls); allDecoratorRoutes.push(...result.decoratorRoutes); allToolDefs.push(...result.toolDefs); + if (result.ormQueries) allORMQueries.push(...result.ormQueries); allConstructorBindings.push(...result.constructorBindings); allTypeEnvBindings.push(...result.typeEnvBindings); } @@ -123,7 +126,7 @@ const processParsingWithWorkers = async ( // Final progress onFileProgress?.(total, total, 'done'); - return { imports: allImports, calls: allCalls, assignments: allAssignments, heritage: allHeritage, routes: allRoutes, fetchCalls: allFetchCalls, decoratorRoutes: allDecoratorRoutes, toolDefs: allToolDefs, constructorBindings: allConstructorBindings, typeEnvBindings: allTypeEnvBindings }; + return { imports: allImports, calls: allCalls, assignments: allAssignments, heritage: allHeritage, routes: allRoutes, fetchCalls: allFetchCalls, decoratorRoutes: allDecoratorRoutes, toolDefs: allToolDefs, ormQueries: allORMQueries, constructorBindings: allConstructorBindings, typeEnvBindings: allTypeEnvBindings }; }; // ============================================================================ diff --git a/gitnexus/src/core/ingestion/pipeline.ts b/gitnexus/src/core/ingestion/pipeline.ts index abff1fd88f..983f9f774e 100644 --- a/gitnexus/src/core/ingestion/pipeline.ts +++ b/gitnexus/src/core/ingestion/pipeline.ts @@ -15,7 +15,7 @@ import { phpFileToRouteURL } from './route-extractors/php.js'; import { extractResponseShapes, extractPHPResponseShapes } from './route-extractors/response-shapes.js'; import { extractMiddlewareChain, extractNextjsMiddlewareConfig, compileMatcher, compiledMatcherMatchesRoute } from './route-extractors/middleware.js'; import { generateId } from '../../lib/utils.js'; -import type { ExtractedFetchCall, ExtractedRoute, ExtractedDecoratorRoute, ExtractedToolDef } from './workers/parse-worker.js'; +import type { ExtractedFetchCall, ExtractedRoute, ExtractedDecoratorRoute, ExtractedToolDef, ExtractedORMQuery } from './workers/parse-worker.js'; import { processHeritage, processHeritageFromExtracted } from './heritage-processor.js'; import { computeMRO } from './mro-processor.js'; import { processCommunities } from './community-processor.js'; @@ -514,6 +514,7 @@ async function runChunkedParseAndResolve( allExtractedRoutes: ExtractedRoute[]; allDecoratorRoutes: ExtractedDecoratorRoute[]; allToolDefs: ExtractedToolDef[]; + allORMQueries: ExtractedORMQuery[]; }> { const symbolTable = ctx.symbols; @@ -634,6 +635,7 @@ async function runChunkedParseAndResolve( const allDecoratorRoutes: ExtractedDecoratorRoute[] = []; // Accumulate MCP/RPC tool definitions (@mcp.tool(), @app.tool(), etc.) const allToolDefs: ExtractedToolDef[] = []; + const allORMQueries: ExtractedORMQuery[] = []; try { for (let chunkIdx = 0; chunkIdx < numChunks; chunkIdx++) { @@ -762,6 +764,9 @@ async function runChunkedParseAndResolve( if (chunkWorkerData.toolDefs?.length) { allToolDefs.push(...chunkWorkerData.toolDefs); } + if (chunkWorkerData.ormQueries?.length) { + allORMQueries.push(...chunkWorkerData.ormQueries); + } } else { await processImports(graph, chunkFiles, astCache, ctx, undefined, repoPath, allPaths); sequentialChunkPaths.push(chunkPaths); @@ -797,6 +802,10 @@ async function runChunkedParseAndResolve( if (chunkFetchCalls.length > 0) { allFetchCalls.push(...chunkFetchCalls); } + // Extract ORM queries (sequential path) + for (const f of chunkFiles) { + extractORMQueriesInline(f.path, f.content, allORMQueries); + } astCache.clear(); } @@ -853,7 +862,7 @@ async function runChunkedParseAndResolve( importCtx.index = EMPTY_INDEX; // Release suffix index memory (~30MB for large repos) importCtx.normalizedFileList = []; - return { exportedTypeMap, allFetchCalls, allExtractedRoutes, allDecoratorRoutes, allToolDefs }; + return { exportedTypeMap, allFetchCalls, allExtractedRoutes, allDecoratorRoutes, allToolDefs, allORMQueries }; } /** @@ -1075,7 +1084,7 @@ export const runPipelineFromRepo = async ( const { scannedFiles, allPaths, totalFiles } = await runScanAndStructure(repoPath, graph, onProgress); // Phase 3+4: Chunked parse + resolve (imports, calls, heritage, routes) - const { exportedTypeMap, allFetchCalls, allExtractedRoutes, allDecoratorRoutes, allToolDefs } = await runChunkedParseAndResolve( + const { exportedTypeMap, allFetchCalls, allExtractedRoutes, allDecoratorRoutes, allToolDefs, allORMQueries } = await runChunkedParseAndResolve( graph, ctx, scannedFiles, allPaths, totalFiles, repoPath, pipelineStart, onProgress, ); @@ -1340,6 +1349,11 @@ export const runPipelineFromRepo = async ( } } + // ── Phase 3.7: ORM Dataflow Detection (Prisma + Supabase) ────────── + if (allORMQueries.length > 0) { + processORMQueries(graph, allORMQueries, isDev); + } + // ── Phase 14: Cross-file binding propagation (topological level sort) ── await runCrossFileBindingPropagation( graph, ctx, exportedTypeMap, allPaths, totalFiles, repoPath, pipelineStart, onProgress, @@ -1374,3 +1388,92 @@ export const runPipelineFromRepo = async ( throw error; } }; + +// Inline ORM regex extraction (avoids importing parse-worker which has worker-only code) +const PRISMA_QUERY_RE = /\bprisma\.(\w+)\.(findMany|findFirst|findUnique|findUniqueOrThrow|findFirstOrThrow|create|createMany|update|updateMany|delete|deleteMany|upsert|count|aggregate|groupBy)\s*\(/g; +const SUPABASE_QUERY_RE = /\bsupabase\.from\s*\(\s*['"](\w+)['"]\s*\)\s*\.(select|insert|update|delete|upsert)\s*\(/g; + +function extractORMQueriesInline(filePath: string, content: string, out: ExtractedORMQuery[]): void { + const hasPrisma = content.includes('prisma.'); + const hasSupabase = content.includes('supabase.from'); + if (!hasPrisma && !hasSupabase) return; + + if (hasPrisma) { + PRISMA_QUERY_RE.lastIndex = 0; + let m; + while ((m = PRISMA_QUERY_RE.exec(content)) !== null) { + const model = m[1]; + if (model.startsWith('$')) continue; + out.push({ filePath, orm: 'prisma', model, method: m[2], lineNumber: content.substring(0, m.index).split('\n').length - 1 }); + } + } + + if (hasSupabase) { + SUPABASE_QUERY_RE.lastIndex = 0; + let m; + while ((m = SUPABASE_QUERY_RE.exec(content)) !== null) { + out.push({ filePath, orm: 'supabase', model: m[1], method: m[2], lineNumber: content.substring(0, m.index).split('\n').length - 1 }); + } + } +} + +// ============================================================================ +// ORM Query Processing — creates QUERIES edges from callers to model nodes +// ============================================================================ + +function processORMQueries( + graph: ReturnType, + queries: ExtractedORMQuery[], + isDev: boolean, +): void { + const modelNodes = new Map(); + const seenEdges = new Set(); + let edgesCreated = 0; + + for (const q of queries) { + const modelKey = `${q.orm}:${q.model}`; + let modelNodeId = modelNodes.get(modelKey); + if (!modelNodeId) { + const candidateIds = [ + generateId('Class', `${q.model}`), + generateId('Interface', `${q.model}`), + generateId('CodeElement', `${q.model}`), + ]; + const existing = candidateIds.find(id => graph.getNode(id)); + if (existing) { + modelNodeId = existing; + } else { + modelNodeId = generateId('CodeElement', `${q.orm}:${q.model}`); + graph.addNode({ + id: modelNodeId, + label: 'CodeElement', + properties: { + name: q.model, + filePath: '', + description: `${q.orm} model/table: ${q.model}`, + }, + }); + } + modelNodes.set(modelKey, modelNodeId); + } + + const fileId = generateId('File', q.filePath); + const edgeKey = `${fileId}->${modelNodeId}:${q.method}`; + if (seenEdges.has(edgeKey)) continue; + seenEdges.add(edgeKey); + + graph.addRelationship({ + id: generateId('QUERIES', edgeKey), + sourceId: fileId, + targetId: modelNodeId, + type: 'QUERIES', + confidence: 0.9, + reason: `${q.orm}-${q.method}`, + }); + edgesCreated++; + } + + if (isDev) { + console.log(`ORM dataflow: ${edgesCreated} QUERIES edges, ${modelNodes.size} models (${queries.length} total calls)`); + } +} diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index 03ea96b4e4..a8dafbb8ca 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -181,6 +181,14 @@ export interface ExtractedToolDef { lineNumber: number; } +export interface ExtractedORMQuery { + filePath: string; + orm: 'prisma' | 'supabase'; + model: string; + method: string; + lineNumber: number; +} + /** Constructor bindings keyed by filePath for cross-file type resolution */ export interface FileConstructorBindings { filePath: string; @@ -206,6 +214,7 @@ export interface ParseWorkerResult { fetchCalls: ExtractedFetchCall[]; decoratorRoutes: ExtractedDecoratorRoute[]; toolDefs: ExtractedToolDef[]; + ormQueries: ExtractedORMQuery[]; constructorBindings: FileConstructorBindings[]; /** File-scope type bindings from TypeEnv fixpoint for exported symbol collection. */ typeEnvBindings: FileTypeEnvBindings[]; @@ -353,6 +362,7 @@ const processBatch = (files: ParseWorkerInput[], onProgress?: (filesProcessed: n fetchCalls: [], decoratorRoutes: [], toolDefs: [], + ormQueries: [], constructorBindings: [], typeEnvBindings: [], skippedLanguages: {}, @@ -825,6 +835,53 @@ function extractLaravelRoutes(tree: any, filePath: string): ExtractedRoute[] { return routes; } +// ============================================================================ +// ORM Query Detection (Prisma + Supabase) +// ============================================================================ + +const PRISMA_QUERY_RE = /\bprisma\.(\w+)\.(findMany|findFirst|findUnique|findUniqueOrThrow|findFirstOrThrow|create|createMany|update|updateMany|delete|deleteMany|upsert|count|aggregate|groupBy)\s*\(/g; +const SUPABASE_QUERY_RE = /\bsupabase\.from\s*\(\s*['"](\w+)['"]\s*\)\s*\.(select|insert|update|delete|upsert)\s*\(/g; + +/** + * Extract ORM query calls from file content via regex. + * Appends results to the provided array (avoids allocation when no matches). + */ +export function extractORMQueries(filePath: string, content: string, out: ExtractedORMQuery[]): void { + const hasPrisma = content.includes('prisma.'); + const hasSupabase = content.includes('supabase.from'); + if (!hasPrisma && !hasSupabase) return; + + if (hasPrisma) { + PRISMA_QUERY_RE.lastIndex = 0; + let m; + while ((m = PRISMA_QUERY_RE.exec(content)) !== null) { + const model = m[1]; + if (model.startsWith('$')) continue; + out.push({ + filePath, + orm: 'prisma', + model, + method: m[2], + lineNumber: content.substring(0, m.index).split('\n').length - 1, + }); + } + } + + if (hasSupabase) { + SUPABASE_QUERY_RE.lastIndex = 0; + let m; + while ((m = SUPABASE_QUERY_RE.exec(content)) !== null) { + out.push({ + filePath, + orm: 'supabase', + model: m[1], + method: m[2], + lineNumber: content.substring(0, m.index).split('\n').length - 1, + }); + } + } +} + const processFileGroup = ( files: ParseWorkerInput[], language: SupportedLanguages, @@ -1346,6 +1403,9 @@ const processFileGroup = ( const extractedRoutes = extractLaravelRoutes(tree, file.path); result.routes.push(...extractedRoutes); } + + // Extract ORM queries (Prisma, Supabase) + extractORMQueries(file.path, file.content, result.ormQueries); } }; @@ -1356,7 +1416,7 @@ const processFileGroup = ( /** Accumulated result across sub-batches */ let accumulated: ParseWorkerResult = { nodes: [], relationships: [], symbols: [], - imports: [], calls: [], assignments: [], heritage: [], routes: [], fetchCalls: [], decoratorRoutes: [], toolDefs: [], constructorBindings: [], typeEnvBindings: [], skippedLanguages: {}, fileCount: 0, + imports: [], calls: [], assignments: [], heritage: [], routes: [], fetchCalls: [], decoratorRoutes: [], toolDefs: [], ormQueries: [], constructorBindings: [], typeEnvBindings: [], skippedLanguages: {}, fileCount: 0, }; let cumulativeProcessed = 0; @@ -1372,6 +1432,7 @@ const mergeResult = (target: ParseWorkerResult, src: ParseWorkerResult) => { target.fetchCalls.push(...src.fetchCalls); target.decoratorRoutes.push(...src.decoratorRoutes); target.toolDefs.push(...src.toolDefs); + target.ormQueries.push(...src.ormQueries); target.constructorBindings.push(...src.constructorBindings); target.typeEnvBindings.push(...src.typeEnvBindings); for (const [lang, count] of Object.entries(src.skippedLanguages)) { @@ -1398,7 +1459,7 @@ parentPort!.on('message', (msg: any) => { if (msg && msg.type === 'flush') { parentPort!.postMessage({ type: 'result', data: accumulated }); // Reset for potential reuse - accumulated = { nodes: [], relationships: [], symbols: [], imports: [], calls: [], assignments: [], heritage: [], routes: [], fetchCalls: [], decoratorRoutes: [], toolDefs: [], constructorBindings: [], typeEnvBindings: [], skippedLanguages: {}, fileCount: 0 }; + accumulated = { nodes: [], relationships: [], symbols: [], imports: [], calls: [], assignments: [], heritage: [], routes: [], fetchCalls: [], decoratorRoutes: [], toolDefs: [], ormQueries: [], constructorBindings: [], typeEnvBindings: [], skippedLanguages: {}, fileCount: 0 }; cumulativeProcessed = 0; return; } diff --git a/gitnexus/src/core/lbug/schema.ts b/gitnexus/src/core/lbug/schema.ts index a01f5f0269..5e61c93370 100644 --- a/gitnexus/src/core/lbug/schema.ts +++ b/gitnexus/src/core/lbug/schema.ts @@ -29,7 +29,7 @@ export const REL_TABLE_NAME = 'CodeRelation'; // Valid relation types // Note: WRAPS is reserved for future middleware graph traversal (not yet emitted) -export const REL_TYPES = ['CONTAINS', 'DEFINES', 'IMPORTS', 'CALLS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'ACCESSES', 'OVERRIDES', 'MEMBER_OF', 'STEP_IN_PROCESS', 'HANDLES_ROUTE', 'FETCHES', 'HANDLES_TOOL', 'ENTRY_POINT_OF', 'WRAPS'] as const; +export const REL_TYPES = ['CONTAINS', 'DEFINES', 'IMPORTS', 'CALLS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'ACCESSES', 'OVERRIDES', 'MEMBER_OF', 'STEP_IN_PROCESS', 'HANDLES_ROUTE', 'FETCHES', 'HANDLES_TOOL', 'ENTRY_POINT_OF', 'WRAPS', 'QUERIES'] as const; export type RelType = typeof REL_TYPES[number]; // ============================================================================ @@ -284,6 +284,7 @@ CREATE REL TABLE ${REL_TABLE_NAME} ( FROM Function TO \`Typedef\`, FROM Function TO \`Union\`, FROM Function TO \`Property\`, + FROM Function TO CodeElement, FROM Class TO Method, FROM Class TO Function, FROM Class TO Class, @@ -317,6 +318,7 @@ CREATE REL TABLE ${REL_TABLE_NAME} ( FROM Method TO Interface, FROM Method TO \`Constructor\`, FROM Method TO \`Property\`, + FROM Method TO CodeElement, FROM \`Template\` TO \`Template\`, FROM \`Template\` TO Function, FROM \`Template\` TO Method, diff --git a/gitnexus/test/fixtures/orm-repo/src/client.ts b/gitnexus/test/fixtures/orm-repo/src/client.ts new file mode 100644 index 0000000000..6b780197c4 --- /dev/null +++ b/gitnexus/test/fixtures/orm-repo/src/client.ts @@ -0,0 +1,3 @@ +import { createClient } from '@supabase/supabase-js'; + +export const supabase = createClient('https://example.supabase.co', 'anon-key'); diff --git a/gitnexus/test/fixtures/orm-repo/src/db.ts b/gitnexus/test/fixtures/orm-repo/src/db.ts new file mode 100644 index 0000000000..9b6c4ce30b --- /dev/null +++ b/gitnexus/test/fixtures/orm-repo/src/db.ts @@ -0,0 +1,3 @@ +import { PrismaClient } from '@prisma/client'; + +export const prisma = new PrismaClient(); diff --git a/gitnexus/test/fixtures/orm-repo/src/prisma-service.ts b/gitnexus/test/fixtures/orm-repo/src/prisma-service.ts new file mode 100644 index 0000000000..923e958fac --- /dev/null +++ b/gitnexus/test/fixtures/orm-repo/src/prisma-service.ts @@ -0,0 +1,25 @@ +import { prisma } from './db'; + +export async function getUsers() { + return prisma.user.findMany({ where: { active: true } }); +} + +export async function createPost(title: string, userId: number) { + return prisma.post.create({ data: { title, authorId: userId } }); +} + +export async function getUserById(id: number) { + return prisma.user.findUnique({ where: { id } }); +} + +export async function updatePost(id: number, title: string) { + return prisma.post.update({ where: { id }, data: { title } }); +} + +export async function deletePost(id: number) { + return prisma.post.delete({ where: { id } }); +} + +export async function countUsers() { + return prisma.user.count(); +} diff --git a/gitnexus/test/fixtures/orm-repo/src/supabase-service.ts b/gitnexus/test/fixtures/orm-repo/src/supabase-service.ts new file mode 100644 index 0000000000..ad2f9e6d0f --- /dev/null +++ b/gitnexus/test/fixtures/orm-repo/src/supabase-service.ts @@ -0,0 +1,17 @@ +import { supabase } from './client'; + +export async function getBookings() { + return supabase.from('bookings').select('*'); +} + +export async function createInterpreter(name: string) { + return supabase.from('interpreters').insert({ name, active: true }); +} + +export async function updateBooking(id: string, status: string) { + return supabase.from('bookings').update({ status }).eq('id', id); +} + +export async function deleteSession(id: string) { + return supabase.from('sessions').delete().eq('id', id); +} diff --git a/gitnexus/test/integration/orm-dataflow.test.ts b/gitnexus/test/integration/orm-dataflow.test.ts new file mode 100644 index 0000000000..b8769d5ec1 --- /dev/null +++ b/gitnexus/test/integration/orm-dataflow.test.ts @@ -0,0 +1,80 @@ +/** + * Integration Tests: ORM Dataflow Detection (Prisma + Supabase) + */ +import { describe, it, expect, beforeAll } from 'vitest'; +import path from 'path'; +import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js'; +import type { PipelineResult } from '../../src/types/pipeline.js'; + +const ORM_REPO = path.resolve(__dirname, '..', 'fixtures', 'orm-repo'); + +describe('ORM dataflow detection', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(ORM_REPO, () => {}); + }, 60000); + + it('creates QUERIES edges for Prisma calls', () => { + const queryEdges: { source: string; target: string; reason: string }[] = []; + for (const rel of result.graph.iterRelationships()) { + if (rel.type === 'QUERIES') { + const source = result.graph.getNode(rel.sourceId); + const target = result.graph.getNode(rel.targetId); + if (source && target) { + queryEdges.push({ + source: source.properties.filePath || source.properties.name, + target: target.properties.name, + reason: rel.reason ?? '', + }); + } + } + } + const prismaEdges = queryEdges.filter(e => e.source.includes('prisma-service')); + const prismaModels = [...new Set(prismaEdges.map(e => e.target))]; + expect(prismaModels).toContain('user'); + expect(prismaModels).toContain('post'); + const reasons = prismaEdges.map(e => e.reason); + expect(reasons.some(r => r.includes('prisma-findMany'))).toBe(true); + expect(reasons.some(r => r.includes('prisma-create'))).toBe(true); + }); + + it('creates QUERIES edges for Supabase calls', () => { + const queryEdges: { source: string; target: string; reason: string }[] = []; + for (const rel of result.graph.iterRelationships()) { + if (rel.type === 'QUERIES') { + const source = result.graph.getNode(rel.sourceId); + const target = result.graph.getNode(rel.targetId); + if (source && target) { + queryEdges.push({ + source: source.properties.filePath || source.properties.name, + target: target.properties.name, + reason: rel.reason ?? '', + }); + } + } + } + const supabaseEdges = queryEdges.filter(e => e.source.includes('supabase-service')); + const supabaseModels = [...new Set(supabaseEdges.map(e => e.target))]; + expect(supabaseModels).toContain('bookings'); + expect(supabaseModels).toContain('interpreters'); + expect(supabaseModels).toContain('sessions'); + const reasons = supabaseEdges.map(e => e.reason); + expect(reasons.some(r => r.includes('supabase-select'))).toBe(true); + expect(reasons.some(r => r.includes('supabase-insert'))).toBe(true); + }); + + it('creates CodeElement nodes for ORM models', () => { + const codeElements: string[] = []; + result.graph.forEachNode(n => { + if (n.label === 'CodeElement' && n.properties.description?.includes('model/table')) { + codeElements.push(n.properties.name); + } + }); + expect(codeElements).toContain('user'); + expect(codeElements).toContain('post'); + expect(codeElements).toContain('bookings'); + expect(codeElements).toContain('interpreters'); + expect(codeElements).toContain('sessions'); + }); +});