useEdgeFunctionsDiff.ts284 lines · main
| 1 | import { basename } from 'path' |
| 2 | import { useQueries, useQueryClient } from '@tanstack/react-query' |
| 3 | import { useCallback, useMemo } from 'react' |
| 4 | |
| 5 | import { |
| 6 | getEdgeFunctionBody, |
| 7 | type EdgeFunctionBodyData, |
| 8 | } from '@/data/edge-functions/edge-function-body-query' |
| 9 | import { |
| 10 | useEdgeFunctionsQuery, |
| 11 | type EdgeFunctionsData, |
| 12 | } from '@/data/edge-functions/edge-functions-query' |
| 13 | import { edgeFunctionsKeys } from '@/data/edge-functions/keys' |
| 14 | |
| 15 | interface UseEdgeFunctionsDiffProps { |
| 16 | currentBranchRef?: string |
| 17 | mainBranchRef?: string |
| 18 | } |
| 19 | |
| 20 | export type FileStatus = 'added' | 'removed' | 'modified' | 'unchanged' |
| 21 | |
| 22 | export interface FileInfo { |
| 23 | key: string |
| 24 | status: FileStatus |
| 25 | } |
| 26 | |
| 27 | export interface FunctionFileInfo { |
| 28 | [functionSlug: string]: FileInfo[] |
| 29 | } |
| 30 | |
| 31 | export interface EdgeFunctionsDiffResult { |
| 32 | addedSlugs: string[] |
| 33 | removedSlugs: string[] |
| 34 | modifiedSlugs: string[] |
| 35 | addedBodiesMap: Record<string, EdgeFunctionBodyData | undefined> |
| 36 | removedBodiesMap: Record<string, EdgeFunctionBodyData | undefined> |
| 37 | currentBodiesMap: Record<string, EdgeFunctionBodyData | undefined> |
| 38 | mainBodiesMap: Record<string, EdgeFunctionBodyData | undefined> |
| 39 | functionFileInfo: FunctionFileInfo |
| 40 | isLoading: boolean |
| 41 | hasChanges: boolean |
| 42 | refetchCurrentBranchFunctions: () => void |
| 43 | refetchMainBranchFunctions: () => void |
| 44 | currentBranchFunctions?: EdgeFunctionsData |
| 45 | mainBranchFunctions?: EdgeFunctionsData |
| 46 | clearDiffsOptimistically: () => void |
| 47 | } |
| 48 | |
| 49 | // Small helper around path.basename but avoids importing the full Node path lib for the browser bundle |
| 50 | const fileKey = (fullPath: string) => basename(fullPath) |
| 51 | |
| 52 | export const useEdgeFunctionsDiff = ({ |
| 53 | currentBranchRef, |
| 54 | mainBranchRef, |
| 55 | }: UseEdgeFunctionsDiffProps): EdgeFunctionsDiffResult => { |
| 56 | const queryClient = useQueryClient() |
| 57 | |
| 58 | // Fetch edge functions for both branches |
| 59 | const { |
| 60 | data: currentBranchFunctions, |
| 61 | isPending: isCurrentFunctionsLoading, |
| 62 | refetch: refetchCurrentBranchFunctions, |
| 63 | } = useEdgeFunctionsQuery( |
| 64 | { projectRef: currentBranchRef }, |
| 65 | { |
| 66 | enabled: !!currentBranchRef, |
| 67 | refetchOnMount: 'always', |
| 68 | staleTime: 30000, // 30 seconds |
| 69 | } |
| 70 | ) |
| 71 | |
| 72 | const { |
| 73 | data: mainBranchFunctions, |
| 74 | isPending: isMainFunctionsLoading, |
| 75 | refetch: refetchMainBranchFunctions, |
| 76 | } = useEdgeFunctionsQuery( |
| 77 | { projectRef: mainBranchRef }, |
| 78 | { |
| 79 | enabled: !!mainBranchRef, |
| 80 | refetchOnMount: 'always', |
| 81 | staleTime: 30000, // 30 seconds |
| 82 | } |
| 83 | ) |
| 84 | |
| 85 | // Identify added / removed / overlapping functions |
| 86 | const { |
| 87 | added = [], |
| 88 | removed = [], |
| 89 | modified = [], |
| 90 | } = useMemo(() => { |
| 91 | if (!currentBranchFunctions || !mainBranchFunctions) { |
| 92 | return { added: [], removed: [], modified: [] } |
| 93 | } |
| 94 | |
| 95 | const currentFuncs = currentBranchFunctions ?? [] |
| 96 | const mainFuncs = mainBranchFunctions ?? [] |
| 97 | |
| 98 | const added = currentFuncs.filter((c) => !mainFuncs.find((m) => m.slug === c.slug)) |
| 99 | const removed = mainFuncs.filter((m) => !currentFuncs.find((c) => c.slug === m.slug)) |
| 100 | const modified = currentFuncs.filter((c) => |
| 101 | mainFuncs.find( |
| 102 | (m) => m.slug === c.slug && (m.ezbr_sha256 === undefined || m.ezbr_sha256 !== c.ezbr_sha256) |
| 103 | ) |
| 104 | ) |
| 105 | |
| 106 | return { added, removed, modified } |
| 107 | }, [currentBranchFunctions, mainBranchFunctions]) |
| 108 | |
| 109 | const addedSlugs = added.map((f) => f.slug) |
| 110 | const removedSlugs = removed.map((f) => f.slug) |
| 111 | const maybeModifiedSlugs = modified.map((f) => f.slug) |
| 112 | |
| 113 | // Fetch function bodies --------------------------------------------------- |
| 114 | const currentBodiesQueries = useQueries({ |
| 115 | queries: maybeModifiedSlugs.map((slug) => ({ |
| 116 | queryKey: ['edge-function-body', currentBranchRef, slug], |
| 117 | queryFn: ({ signal }: { signal?: AbortSignal }) => |
| 118 | getEdgeFunctionBody({ projectRef: currentBranchRef, slug }, signal), |
| 119 | enabled: !!currentBranchRef, |
| 120 | refetchOnMount: 'always' as const, |
| 121 | })), |
| 122 | }) |
| 123 | |
| 124 | const mainBodiesQueries = useQueries({ |
| 125 | queries: maybeModifiedSlugs.map((slug) => ({ |
| 126 | queryKey: ['edge-function-body', mainBranchRef, slug], |
| 127 | queryFn: ({ signal }: { signal?: AbortSignal }) => |
| 128 | getEdgeFunctionBody({ projectRef: mainBranchRef, slug }, signal), |
| 129 | enabled: !!mainBranchRef, |
| 130 | refetchOnMount: 'always' as const, |
| 131 | })), |
| 132 | }) |
| 133 | |
| 134 | const addedBodiesQueries = useQueries({ |
| 135 | queries: addedSlugs.map((slug) => ({ |
| 136 | queryKey: ['edge-function-body', currentBranchRef, slug], |
| 137 | queryFn: ({ signal }: { signal?: AbortSignal }) => |
| 138 | getEdgeFunctionBody({ projectRef: currentBranchRef, slug }, signal), |
| 139 | enabled: !!currentBranchRef, |
| 140 | refetchOnMount: 'always' as const, |
| 141 | })), |
| 142 | }) |
| 143 | |
| 144 | const removedBodiesQueries = useQueries({ |
| 145 | queries: removedSlugs.map((slug) => ({ |
| 146 | queryKey: ['edge-function-body', mainBranchRef, slug], |
| 147 | queryFn: ({ signal }: { signal?: AbortSignal }) => |
| 148 | getEdgeFunctionBody({ projectRef: mainBranchRef, slug }, signal), |
| 149 | enabled: !!mainBranchRef, |
| 150 | refetchOnMount: 'always' as const, |
| 151 | })), |
| 152 | }) |
| 153 | |
| 154 | // Flatten loading flags ---------------------------------------------------- |
| 155 | const isLoading = |
| 156 | [ |
| 157 | ...currentBodiesQueries, |
| 158 | ...mainBodiesQueries, |
| 159 | ...addedBodiesQueries, |
| 160 | ...removedBodiesQueries, |
| 161 | ].some((q) => q.isLoading) || |
| 162 | isCurrentFunctionsLoading || |
| 163 | isMainFunctionsLoading |
| 164 | |
| 165 | // Build lookup maps -------------------------------------------------------- |
| 166 | const currentBodiesMap: Record<string, EdgeFunctionBodyData | undefined> = {} |
| 167 | currentBodiesQueries.forEach((q, idx) => { |
| 168 | if (q.data) currentBodiesMap[maybeModifiedSlugs[idx]] = q.data |
| 169 | }) |
| 170 | |
| 171 | const mainBodiesMap: Record<string, EdgeFunctionBodyData | undefined> = {} |
| 172 | mainBodiesQueries.forEach((q, idx) => { |
| 173 | if (q.data) mainBodiesMap[maybeModifiedSlugs[idx]] = q.data |
| 174 | }) |
| 175 | |
| 176 | const addedBodiesMap: Record<string, EdgeFunctionBodyData | undefined> = {} |
| 177 | addedBodiesQueries.forEach((q, idx) => { |
| 178 | if (q.data) addedBodiesMap[addedSlugs[idx]] = q.data |
| 179 | }) |
| 180 | |
| 181 | const removedBodiesMap: Record<string, EdgeFunctionBodyData | undefined> = {} |
| 182 | removedBodiesQueries.forEach((q, idx) => { |
| 183 | if (q.data) removedBodiesMap[removedSlugs[idx]] = q.data |
| 184 | }) |
| 185 | |
| 186 | // Determine modified slugs and build file info ----------------------------- |
| 187 | const modifiedSlugs: string[] = [] |
| 188 | const functionFileInfo: FunctionFileInfo = {} |
| 189 | |
| 190 | // Process overlapping functions to determine modifications and file info |
| 191 | maybeModifiedSlugs.forEach((slug) => { |
| 192 | const currentBody = currentBodiesMap[slug] |
| 193 | const mainBody = mainBodiesMap[slug] |
| 194 | if (!currentBody || !mainBody) return |
| 195 | |
| 196 | const allFileKeys = new Set( |
| 197 | [...currentBody.files, ...mainBody.files].map((f) => fileKey(f.name)) |
| 198 | ) |
| 199 | const fileInfos: FileInfo[] = [] |
| 200 | let hasModifications = false |
| 201 | |
| 202 | for (const key of allFileKeys) { |
| 203 | const currentFile = currentBody.files.find((f) => fileKey(f.name) === key) |
| 204 | const mainFile = mainBody.files.find((f) => fileKey(f.name) === key) |
| 205 | |
| 206 | let status: FileStatus = 'unchanged' |
| 207 | |
| 208 | if (!currentFile && mainFile) { |
| 209 | status = 'removed' |
| 210 | hasModifications = true |
| 211 | } else if (currentFile && !mainFile) { |
| 212 | status = 'added' |
| 213 | hasModifications = true |
| 214 | } else if (currentFile && mainFile && currentFile.content !== mainFile.content) { |
| 215 | status = 'modified' |
| 216 | hasModifications = true |
| 217 | } |
| 218 | |
| 219 | fileInfos.push({ key, status }) |
| 220 | } |
| 221 | |
| 222 | if (hasModifications) { |
| 223 | modifiedSlugs.push(slug) |
| 224 | functionFileInfo[slug] = fileInfos |
| 225 | } |
| 226 | }) |
| 227 | |
| 228 | // Add file info for added functions |
| 229 | addedSlugs.forEach((slug) => { |
| 230 | const body = addedBodiesMap[slug] |
| 231 | if (body) { |
| 232 | functionFileInfo[slug] = body.files.map((file) => ({ |
| 233 | key: fileKey(file.name), |
| 234 | status: 'added' as FileStatus, |
| 235 | })) |
| 236 | } |
| 237 | }) |
| 238 | |
| 239 | // Add file info for removed functions |
| 240 | removedSlugs.forEach((slug) => { |
| 241 | const body = removedBodiesMap[slug] |
| 242 | if (body) { |
| 243 | functionFileInfo[slug] = body.files.map((file) => ({ |
| 244 | key: fileKey(file.name), |
| 245 | status: 'removed' as FileStatus, |
| 246 | })) |
| 247 | } |
| 248 | }) |
| 249 | |
| 250 | const hasChanges = addedSlugs.length > 0 || removedSlugs.length > 0 || modifiedSlugs.length > 0 |
| 251 | |
| 252 | const clearDiffsOptimistically = useCallback(() => { |
| 253 | if (!currentBranchRef || !mainBranchFunctions) return |
| 254 | |
| 255 | queryClient.setQueryData(edgeFunctionsKeys.list(currentBranchRef), mainBranchFunctions) |
| 256 | |
| 257 | mainBranchFunctions.forEach((func) => { |
| 258 | const mainBody = mainBodiesMap[func.slug] |
| 259 | if (mainBody) { |
| 260 | queryClient.setQueryData(['edge-function-body', currentBranchRef, func.slug], mainBody) |
| 261 | } |
| 262 | }) |
| 263 | }, [currentBranchRef, mainBranchFunctions, mainBodiesMap, queryClient]) |
| 264 | |
| 265 | return { |
| 266 | addedSlugs, |
| 267 | removedSlugs, |
| 268 | modifiedSlugs, |
| 269 | addedBodiesMap, |
| 270 | removedBodiesMap, |
| 271 | currentBodiesMap, |
| 272 | mainBodiesMap, |
| 273 | functionFileInfo, |
| 274 | isLoading, |
| 275 | hasChanges, |
| 276 | refetchCurrentBranchFunctions, |
| 277 | refetchMainBranchFunctions, |
| 278 | currentBranchFunctions, |
| 279 | mainBranchFunctions, |
| 280 | clearDiffsOptimistically, |
| 281 | } |
| 282 | } |
| 283 | |
| 284 | export default useEdgeFunctionsDiff |