|
| 1 | +/** |
| 2 | + * GET /api/projects/[id]/specs/[spec]/dependency-graph - Get complete dependency graph for a spec (multi-project mode) |
| 3 | + * |
| 4 | + * Returns upstream dependencies, downstream dependents for the specified spec |
| 5 | + * in multi-project mode where specs are loaded from project registries. |
| 6 | + */ |
| 7 | + |
| 8 | +import { NextResponse } from 'next/server'; |
| 9 | +import { getSpecById, getSpecsWithMetadata } from '@/lib/db/service-queries'; |
| 10 | + |
| 11 | +/** |
| 12 | + * Helper to extract title from name |
| 13 | + */ |
| 14 | +function getTitle(name: string): string { |
| 15 | + const parts = name.split('-'); |
| 16 | + if (parts.length > 1) { |
| 17 | + return parts.slice(1).join('-').replace(/-/g, ' '); |
| 18 | + } |
| 19 | + return name; |
| 20 | +} |
| 21 | + |
| 22 | +export async function GET( |
| 23 | + request: Request, |
| 24 | + { params }: { params: Promise<{ id: string; spec: string }> } |
| 25 | +) { |
| 26 | + try { |
| 27 | + const { id: projectId, spec: specId } = await params; |
| 28 | + |
| 29 | + // Get the target spec |
| 30 | + const spec = await getSpecById(specId, projectId); |
| 31 | + if (!spec) { |
| 32 | + return NextResponse.json( |
| 33 | + { error: 'Spec not found' }, |
| 34 | + { status: 404 } |
| 35 | + ); |
| 36 | + } |
| 37 | + |
| 38 | + // Get all specs with metadata to build the dependency graph |
| 39 | + const allSpecs = await getSpecsWithMetadata(projectId); |
| 40 | + |
| 41 | + // Build lookup maps |
| 42 | + const specByName = new Map<string, typeof allSpecs[0]>(); |
| 43 | + const specByNumber = new Map<string, typeof allSpecs[0]>(); |
| 44 | + |
| 45 | + for (const s of allSpecs) { |
| 46 | + specByName.set(s.specName, s); |
| 47 | + if (s.specNumber) { |
| 48 | + specByNumber.set(s.specNumber.toString(), s); |
| 49 | + specByNumber.set(s.specNumber.toString().padStart(3, '0'), s); |
| 50 | + } |
| 51 | + } |
| 52 | + |
| 53 | + // Find this spec's relationships |
| 54 | + const currentSpec = allSpecs.find(s => |
| 55 | + s.specNumber?.toString() === specId || |
| 56 | + s.specNumber?.toString().padStart(3, '0') === specId || |
| 57 | + s.specName === specId || |
| 58 | + s.id === specId |
| 59 | + ); |
| 60 | + |
| 61 | + if (!currentSpec) { |
| 62 | + return NextResponse.json( |
| 63 | + { error: 'Spec not found in project' }, |
| 64 | + { status: 404 } |
| 65 | + ); |
| 66 | + } |
| 67 | + |
| 68 | + // Resolve dependsOn specs |
| 69 | + const dependsOnSpecs = currentSpec.relationships.dependsOn |
| 70 | + .map(dep => { |
| 71 | + const match = dep.match(/^(\d+)/); |
| 72 | + if (match) { |
| 73 | + return specByNumber.get(match[1]) || specByNumber.get(match[1].padStart(3, '0')); |
| 74 | + } |
| 75 | + return specByName.get(dep); |
| 76 | + }) |
| 77 | + .filter((s): s is typeof allSpecs[0] => s !== undefined); |
| 78 | + |
| 79 | + // Resolve requiredBy specs |
| 80 | + const requiredBySpecs = currentSpec.relationships.requiredBy |
| 81 | + .map(dep => { |
| 82 | + const match = dep.match(/^(\d+)/); |
| 83 | + if (match) { |
| 84 | + return specByNumber.get(match[1]) || specByNumber.get(match[1].padStart(3, '0')); |
| 85 | + } |
| 86 | + return specByName.get(dep); |
| 87 | + }) |
| 88 | + .filter((s): s is typeof allSpecs[0] => s !== undefined); |
| 89 | + |
| 90 | + // Format response with simplified spec metadata |
| 91 | + const response = { |
| 92 | + current: { |
| 93 | + id: spec.id, |
| 94 | + specNumber: spec.specNumber, |
| 95 | + specName: spec.specName, |
| 96 | + title: spec.title, |
| 97 | + status: spec.status, |
| 98 | + priority: spec.priority, |
| 99 | + }, |
| 100 | + dependsOn: dependsOnSpecs.map(s => ({ |
| 101 | + specNumber: s.specNumber, |
| 102 | + specName: s.specName, |
| 103 | + title: s.title || getTitle(s.specName), |
| 104 | + status: s.status, |
| 105 | + priority: s.priority, |
| 106 | + })), |
| 107 | + requiredBy: requiredBySpecs.map(s => ({ |
| 108 | + specNumber: s.specNumber, |
| 109 | + specName: s.specName, |
| 110 | + title: s.title || getTitle(s.specName), |
| 111 | + status: s.status, |
| 112 | + priority: s.priority, |
| 113 | + })), |
| 114 | + }; |
| 115 | + |
| 116 | + // Return with cache headers |
| 117 | + return NextResponse.json(response, { |
| 118 | + headers: { |
| 119 | + 'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=120' |
| 120 | + } |
| 121 | + }); |
| 122 | + } catch (error) { |
| 123 | + console.error('Error fetching dependency graph:', error); |
| 124 | + return NextResponse.json( |
| 125 | + { error: 'Failed to fetch dependency graph' }, |
| 126 | + { status: 500 } |
| 127 | + ); |
| 128 | + } |
| 129 | +} |
0 commit comments