sandbox.core.ts74 lines · main
| 1 | import { PGliteWorker } from '@electric-sql/pglite/worker' |
| 2 | |
| 3 | import { SANDBOX_SETUP_STATEMENTS } from './sandbox.constants' |
| 4 | import { applySchema, applySeed } from './sandbox.utils' |
| 5 | import { type DatabaseSchemaDDLData } from '@/data/rls-tester/get-schema-ddl' |
| 6 | import { type TableSeedData } from '@/data/rls-tester/get-seed-data' |
| 7 | import { getErrorMessage } from '@/lib/get-error-message' |
| 8 | |
| 9 | type RLSTestResult = Record<string, unknown>[] |
| 10 | |
| 11 | export 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 | |
| 18 | let instance: SandboxCore | null = null |
| 19 | let initPromise: Promise<SandboxCore> | null = null |
| 20 | |
| 21 | export 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 | |
| 31 | const 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 | } |