import { createServer } from 'node:http'; import { LibsqlError, createClient } from '@libsql/client'; import { z } from 'zod'; import { cli } from '../dist/core/cli/index.js'; import { resolveDbConfig } from '../dist/core/load-file.js'; import { getCreateIndexQueries, getCreateTableQuery } from '../dist/core/queries.js'; const singleQuerySchema = z.object({ sql: z.string(), args: z.array(z.any()).or(z.record(z.string(), z.any())), }); const querySchema = singleQuerySchema.or(z.array(singleQuerySchema)); let portIncrementer = 8030; /** * @param {import('astro').AstroConfig} astroConfig * @param {number | undefined} port */ export async function setupRemoteDbServer(astroConfig) { const port = portIncrementer++; process.env.ASTRO_STUDIO_REMOTE_DB_URL = `http://localhost:${port}`; process.env.ASTRO_INTERNAL_TEST_REMOTE = true; const server = createRemoteDbServer().listen(port); const { dbConfig } = await resolveDbConfig(astroConfig); const setupQueries = []; for (const [name, table] of Object.entries(dbConfig?.tables ?? {})) { const createQuery = getCreateTableQuery(name, table); const indexQueries = getCreateIndexQueries(name, table); setupQueries.push(createQuery, ...indexQueries); } await fetch(`http://localhost:${port}/db/query`, { method: 'POST', body: JSON.stringify(setupQueries.map((sql) => ({ sql, args: [] }))), headers: { 'Content-Type': 'application/json', }, }); await cli({ config: astroConfig, flags: { _: [undefined, 'astro', 'db', 'execute', 'db/seed.ts'], remote: true, }, }); return { server, async stop() { delete process.env.ASTRO_STUDIO_REMOTE_DB_URL; delete process.env.ASTRO_INTERNAL_TEST_REMOTE; return new Promise((resolve, reject) => { server.close((err) => { if (err) { reject(err); } else { resolve(); } }); }); }, }; } function createRemoteDbServer() { const dbClient = createClient({ url: ':memory:', }); const server = createServer((req, res) => { if ( !req.url.startsWith('/db/query') || req.method !== 'POST' || req.headers['content-type'] !== 'application/json' ) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end( JSON.stringify({ success: false, }) ); return; } const rawBody = []; req.on('data', (chunk) => { rawBody.push(chunk); }); req.on('end', async () => { let json; try { json = JSON.parse(Buffer.concat(rawBody).toString()); } catch (e) { applyParseError(res); return; } const parsed = querySchema.safeParse(json); if (parsed.success === false) { applyParseError(res); return; } const body = parsed.data; try { const result = Array.isArray(body) ? await dbClient.batch(body) : await dbClient.execute(body); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(result)); } catch (e) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.statusMessage = e.message; res.end( JSON.stringify({ success: false, error: { code: e instanceof LibsqlError ? e.code : 'SQLITE_QUERY_FAILED', details: e.message, }, }) ); } }); }); server.on('close', () => { dbClient.close(); }); return server; } function applyParseError(res) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.statusMessage = 'Invalid request body'; res.end( JSON.stringify({ // Use JSON response with `success: boolean` property // to match remote error responses. success: false, }) ); }