diff options
Diffstat (limited to 'packages/db/src/runtime/db-client.ts')
-rw-r--r-- | packages/db/src/runtime/db-client.ts | 183 |
1 files changed, 3 insertions, 180 deletions
diff --git a/packages/db/src/runtime/db-client.ts b/packages/db/src/runtime/db-client.ts index 55288951d..e45a2d717 100644 --- a/packages/db/src/runtime/db-client.ts +++ b/packages/db/src/runtime/db-client.ts @@ -1,10 +1,7 @@ -import type { InStatement } from '@libsql/client'; import { type Config as LibSQLConfig, createClient } from '@libsql/client'; import type { LibSQLDatabase } from 'drizzle-orm/libsql'; import { drizzle as drizzleLibsql } from 'drizzle-orm/libsql'; -import { type SqliteRemoteDatabase, drizzle as drizzleProxy } from 'drizzle-orm/sqlite-proxy'; -import { z } from 'zod'; -import { DetailedLibsqlError, safeFetch } from './utils.js'; +import type { SqliteRemoteDatabase } from 'drizzle-orm/sqlite-proxy'; const isWebContainer = !!process.versions?.webcontainer; @@ -34,16 +31,8 @@ export function createLocalDatabaseClient(options: LocalDbClientOptions): LibSQL return db; } -const remoteResultSchema = z.object({ - columns: z.array(z.string()), - columnTypes: z.array(z.string()), - rows: z.array(z.array(z.unknown())), - rowsAffected: z.number(), - lastInsertRowid: z.unknown().optional(), -}); - type RemoteDbClientOptions = { - dbType: 'studio' | 'libsql'; + dbType: 'libsql'; appToken: string; remoteUrl: string | URL; }; @@ -51,9 +40,7 @@ type RemoteDbClientOptions = { export function createRemoteDatabaseClient(options: RemoteDbClientOptions) { const remoteUrl = new URL(options.remoteUrl); - return options.dbType === 'studio' - ? createStudioDatabaseClient(options.appToken, remoteUrl) - : createRemoteLibSQLClient(options.appToken, remoteUrl, options.remoteUrl.toString()); + return createRemoteLibSQLClient(options.appToken, remoteUrl, options.remoteUrl.toString()); } // this function parses the options from a `Record<string, string>` @@ -99,167 +86,3 @@ function createRemoteLibSQLClient(appToken: string, remoteDbURL: URL, rawUrl: st const client = createClient({ ...parseOpts(options), url, authToken: appToken }); return drizzleLibsql(client); } - -function createStudioDatabaseClient(appToken: string, remoteDbURL: URL) { - if (appToken == null) { - throw new Error(`Cannot create a remote client: missing app token.`); - } - - const url = new URL('/db/query', remoteDbURL); - - const db = drizzleProxy( - async (sql, parameters, method) => { - const requestBody: InStatement = { sql, args: parameters }; - const res = await safeFetch( - url, - { - method: 'POST', - headers: { - Authorization: `Bearer ${appToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(requestBody), - }, - async (response) => { - throw await parseRemoteError(response); - }, - ); - - let remoteResult: z.infer<typeof remoteResultSchema>; - try { - const json = await res.json(); - remoteResult = remoteResultSchema.parse(json); - } catch { - throw new DetailedLibsqlError({ - message: await getUnexpectedResponseMessage(res), - code: KNOWN_ERROR_CODES.SQL_QUERY_FAILED, - }); - } - - if (method === 'run') { - const rawRows = Array.from(remoteResult.rows); - // Implement basic `toJSON()` for Drizzle to serialize properly - (remoteResult as any).rows.toJSON = () => rawRows; - // Using `db.run()` drizzle massages the rows into an object. - // So in order to make dev/prod consistent, we need to do the same here. - // This creates an object and loops over each row replacing it with the object. - for (let i = 0; i < remoteResult.rows.length; i++) { - let row = remoteResult.rows[i]; - let item: Record<string, any> = {}; - remoteResult.columns.forEach((col, index) => { - item[col] = row[index]; - }); - (remoteResult as any).rows[i] = item; - } - return remoteResult; - } - - // Drizzle expects each row as an array of its values - const rowValues: unknown[][] = []; - - for (const row of remoteResult.rows) { - if (row != null && typeof row === 'object') { - rowValues.push(Object.values(row)); - } - } - - if (method === 'get') { - return { rows: rowValues[0] }; - } - - return { rows: rowValues }; - }, - async (queries) => { - const stmts: InStatement[] = queries.map(({ sql, params }) => ({ sql, args: params })); - const res = await safeFetch( - url, - { - method: 'POST', - headers: { - Authorization: `Bearer ${appToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(stmts), - }, - async (response) => { - throw await parseRemoteError(response); - }, - ); - - let remoteResults: z.infer<typeof remoteResultSchema>[]; - try { - const json = await res.json(); - remoteResults = z.array(remoteResultSchema).parse(json); - } catch { - throw new DetailedLibsqlError({ - message: await getUnexpectedResponseMessage(res), - code: KNOWN_ERROR_CODES.SQL_QUERY_FAILED, - }); - } - let results: any[] = []; - for (const [idx, rawResult] of remoteResults.entries()) { - if (queries[idx]?.method === 'run') { - results.push(rawResult); - continue; - } - - // Drizzle expects each row as an array of its values - const rowValues: unknown[][] = []; - - for (const row of rawResult.rows) { - if (row != null && typeof row === 'object') { - rowValues.push(Object.values(row)); - } - } - - if (queries[idx]?.method === 'get') { - results.push({ rows: rowValues[0] }); - } - - results.push({ rows: rowValues }); - } - return results; - }, - ); - applyTransactionNotSupported(db); - return db; -} - -const errorSchema = z.object({ - success: z.boolean(), - error: z.object({ - code: z.string(), - details: z.string().optional(), - }), -}); - -const KNOWN_ERROR_CODES = { - SQL_QUERY_FAILED: 'SQL_QUERY_FAILED', -}; - -const getUnexpectedResponseMessage = async (response: Response) => - `Unexpected response from remote database:\n(Status ${response.status}) ${await response - .clone() - .text()}`; - -async function parseRemoteError(response: Response): Promise<DetailedLibsqlError> { - let error; - try { - error = errorSchema.parse(await response.clone().json()).error; - } catch { - return new DetailedLibsqlError({ - message: await getUnexpectedResponseMessage(response), - code: KNOWN_ERROR_CODES.SQL_QUERY_FAILED, - }); - } - // Strip LibSQL error prefixes - let baseDetails = - error.details?.replace(/.*SQLite error: /, '') ?? 'Error querying remote database.'; - // Remove duplicated "code" in details - const details = baseDetails.slice(baseDetails.indexOf(':') + 1).trim(); - let hint = `See the Astro DB guide for query and push instructions: https://docs.astro.build/en/guides/astro-db/#query-your-database`; - if (error.code === KNOWN_ERROR_CODES.SQL_QUERY_FAILED && details.includes('no such table')) { - hint = `Did you run \`astro db push\` to push your latest table schemas?`; - } - return new DetailedLibsqlError({ message: details, code: error.code, hint }); -} |