useEdgeFunctionsDiff.ts 8.8 KB

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