sandbox.tsx 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. import { noop } from 'lodash'
  2. import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react'
  3. import { toast } from 'sonner'
  4. import { AUTH_USERS_SEED_TABLE } from './sandbox.constants'
  5. import { getSandboxCore, type SandboxCore } from './sandbox.core'
  6. import { getDatabaseSchemaDDL } from '@/data/rls-tester/get-schema-ddl'
  7. import { getProjectSeedData, TableSeedData } from '@/data/rls-tester/get-seed-data'
  8. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  9. import { getErrorMessage } from '@/lib/get-error-message'
  10. type SandboxStatus = 'idle' | 'loading' | 'ready' | 'error'
  11. const SandboxContext = createContext<{
  12. status: SandboxStatus
  13. error?: string
  14. sandbox: SandboxCore | null
  15. isSyncing: boolean
  16. startSandbox: () => void
  17. destroySandbox: () => Promise<void>
  18. syncSandbox: () => Promise<void>
  19. }>({
  20. status: 'idle',
  21. error: undefined,
  22. sandbox: null,
  23. isSyncing: false,
  24. startSandbox: noop,
  25. destroySandbox: async () => {},
  26. syncSandbox: async () => {},
  27. })
  28. export const PostgresSandboxProvider = ({ children }: PropsWithChildren) => {
  29. const { data: project } = useSelectedProjectQuery()
  30. const [start, setStart] = useState<boolean>(false)
  31. const [error, setError] = useState<string>()
  32. const [sandbox, setSandbox] = useState<SandboxCore | null>(null)
  33. const [status, setStatus] = useState<SandboxStatus>('idle')
  34. const [isSyncing, setIsSyncing] = useState(false)
  35. const destroySandbox = async () => {
  36. if (isSyncing) return
  37. if (!sandbox) return console.error('Sandbox is not set up')
  38. await sandbox.destroy()
  39. setSandbox(null)
  40. setStatus('idle')
  41. setError(undefined)
  42. setStart(false)
  43. }
  44. // Internal — takes the target explicitly so the boot path can pass the
  45. // freshly booted core before React state has caught up. Callers outside
  46. // the provider use `syncSandbox()` which sources the target from state.
  47. const applyToCore = async (target: SandboxCore) => {
  48. setIsSyncing(true)
  49. try {
  50. const schemaDDL = await getDatabaseSchemaDDL({
  51. projectRef: project?.ref,
  52. connectionString: project?.connectionString,
  53. schemas: ['public'],
  54. })
  55. const seedData: TableSeedData[] = await getProjectSeedData({
  56. projectRef: project?.ref,
  57. connectionString: project?.connectionString,
  58. tables: [AUTH_USERS_SEED_TABLE, ...(schemaDDL.rlsStatuses ?? [])],
  59. rowLimit: 100,
  60. })
  61. await target.setSchema(schemaDDL)
  62. await target.setSeed(seedData)
  63. } catch (e) {
  64. const message = getErrorMessage(e) ?? String(e)
  65. if (sandbox) {
  66. // Refresh path — sandbox is still usable with the previous schema/data.
  67. toast.error(`Failed to refresh sandbox: ${message}`)
  68. } else {
  69. // Boot path — propagate so the outer .catch sets status='error' and
  70. // the SandboxManagement error branch renders.
  71. throw e
  72. }
  73. } finally {
  74. setIsSyncing(false)
  75. }
  76. }
  77. const syncSandbox = async () => {
  78. if (isSyncing) return
  79. if (!sandbox) return console.error('Sandbox has not been loaded')
  80. await applyToCore(sandbox)
  81. }
  82. useEffect(() => {
  83. if (!start) return
  84. let cancelled = false
  85. setStatus('loading')
  86. getSandboxCore()
  87. .then(async (core) => {
  88. if (cancelled) return
  89. await applyToCore(core)
  90. setSandbox(core)
  91. setStatus('ready')
  92. })
  93. .catch((error) => {
  94. if (cancelled) return
  95. setError(getErrorMessage(error) ?? '')
  96. setStatus('error')
  97. setStart(false)
  98. })
  99. return () => {
  100. cancelled = true
  101. }
  102. // applyToCore intentionally omitted: this effect should fire once when
  103. // `start` flips, not every time the helper identity changes.
  104. // eslint-disable-next-line react-hooks/exhaustive-deps
  105. }, [start])
  106. return (
  107. <SandboxContext.Provider
  108. value={{
  109. status,
  110. error,
  111. sandbox,
  112. isSyncing,
  113. startSandbox: () => setStart(true),
  114. destroySandbox,
  115. syncSandbox,
  116. }}
  117. >
  118. {children}
  119. </SandboxContext.Provider>
  120. )
  121. }
  122. export const usePostgresSandbox = () => useContext(SandboxContext)