useEdgeFunctionsDiff.ts284 lines · main
1import { basename } from 'path'
2import { useQueries, useQueryClient } from '@tanstack/react-query'
3import { useCallback, useMemo } from 'react'
4
5import {
6 getEdgeFunctionBody,
7 type EdgeFunctionBodyData,
8} from '@/data/edge-functions/edge-function-body-query'
9import {
10 useEdgeFunctionsQuery,
11 type EdgeFunctionsData,
12} from '@/data/edge-functions/edge-functions-query'
13import { edgeFunctionsKeys } from '@/data/edge-functions/keys'
14
15interface UseEdgeFunctionsDiffProps {
16 currentBranchRef?: string
17 mainBranchRef?: string
18}
19
20export type FileStatus = 'added' | 'removed' | 'modified' | 'unchanged'
21
22export interface FileInfo {
23 key: string
24 status: FileStatus
25}
26
27export interface FunctionFileInfo {
28 [functionSlug: string]: FileInfo[]
29}
30
31export 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
50const fileKey = (fullPath: string) => basename(fullPath)
51
52export 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
284export default useEdgeFunctionsDiff