sandbox.core.ts74 lines · main
1import { PGliteWorker } from '@electric-sql/pglite/worker'
2
3import { SANDBOX_SETUP_STATEMENTS } from './sandbox.constants'
4import { applySchema, applySeed } from './sandbox.utils'
5import { type DatabaseSchemaDDLData } from '@/data/rls-tester/get-schema-ddl'
6import { type TableSeedData } from '@/data/rls-tester/get-seed-data'
7import { getErrorMessage } from '@/lib/get-error-message'
8
9type RLSTestResult = Record<string, unknown>[]
10
11export interface SandboxCore {
12 setSchema(data: DatabaseSchemaDDLData): Promise<void>
13 setSeed(tables: TableSeedData[]): Promise<void>
14 destroy(): Promise<void>
15 run: (props: { sql: string }) => Promise<{ result: RLSTestResult }>
16}
17
18let instance: SandboxCore | null = null
19let initPromise: Promise<SandboxCore> | null = null
20
21export const getSandboxCore = async () => {
22 if (instance) return instance
23 if (!initPromise) {
24 initPromise = boot().finally(() => {
25 initPromise = null
26 })
27 }
28 return initPromise
29}
30
31const boot = async (): Promise<SandboxCore> => {
32 const webWorker = new Worker(new URL('./pglite.worker.ts', import.meta.url), { type: 'module' })
33 const pg = await PGliteWorker.create(webWorker)
34
35 for (const sql of SANDBOX_SETUP_STATEMENTS) {
36 try {
37 await pg.exec(sql)
38 } catch (err) {
39 console.warn('[Postgres sandbox] setup:', (err as Error).message, `— ${sql.slice(0, 60)}`)
40 }
41 }
42
43 function makeExecutor() {
44 return { execSql: (sql: string) => pg.exec(sql).then(() => undefined as void) }
45 }
46
47 async function setSchema(data: DatabaseSchemaDDLData): Promise<void> {
48 await applySchema(makeExecutor(), data)
49 }
50
51 async function setSeed(tables: TableSeedData[]): Promise<void> {
52 await applySeed(makeExecutor(), tables)
53 }
54
55 const run = async ({ sql }: { sql: string }) => {
56 try {
57 // [Joshen] First 2 results will be from role impersonation, the actual result from the
58 // query will be returned as the 3rd result.
59 const results = await pg.exec(sql)
60 return { result: results[2].rows ?? [] }
61 } catch (error) {
62 await pg.exec('ROLLBACK').catch(() => {})
63 throw error instanceof Error ? error : new Error(getErrorMessage(error) ?? String(error))
64 }
65 }
66
67 const destroy = async () => {
68 webWorker.terminate()
69 instance = null
70 }
71
72 instance = { run, destroy, setSchema, setSeed }
73 return instance
74}