Files
turso/packages/turso-serverless/src/session.ts
Pekka Enberg ced6fba624 serverless: Add DatabasError type
Add an explicit error type so application can actually do error handling...
2025-07-24 16:10:18 +03:00

217 lines
5.8 KiB
TypeScript

import {
executeCursor,
encodeValue,
decodeValue,
type CursorRequest,
type CursorResponse,
type CursorEntry
} from './protocol.js';
import { DatabaseError } from './error.js';
/**
* Configuration options for a session.
*/
export interface SessionConfig {
/** Database URL */
url: string;
/** Authentication token */
authToken: string;
}
function normalizeUrl(url: string): string {
return url.replace(/^libsql:\/\//, 'https://');
}
function isValidIdentifier(str: string): boolean {
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(str);
}
/**
* A database session that manages the connection state and baton.
*
* Each session maintains its own connection state and can execute SQL statements
* independently without interfering with other sessions.
*/
export class Session {
private config: SessionConfig;
private baton: string | null = null;
private baseUrl: string;
constructor(config: SessionConfig) {
this.config = config;
this.baseUrl = normalizeUrl(config.url);
}
/**
* Execute a SQL statement and return all results.
*
* @param sql - The SQL statement to execute
* @param args - Optional array of parameter values
* @returns Promise resolving to the complete result set
*/
async execute(sql: string, args: any[] = []): Promise<any> {
const { response, entries } = await this.executeRaw(sql, args);
const result = await this.processCursorEntries(entries);
return result;
}
/**
* Execute a SQL statement and return the raw response and entries.
*
* @param sql - The SQL statement to execute
* @param args - Optional array of parameter values
* @returns Promise resolving to the raw response and cursor entries
*/
async executeRaw(sql: string, args: any[] = []): Promise<{ response: CursorResponse; entries: AsyncGenerator<CursorEntry> }> {
const request: CursorRequest = {
baton: this.baton,
batch: {
steps: [{
stmt: {
sql,
args: args.map(encodeValue),
want_rows: true
}
}]
}
};
const { response, entries } = await executeCursor(this.baseUrl, this.config.authToken, request);
this.baton = response.baton;
if (response.base_url) {
this.baseUrl = response.base_url;
}
return { response, entries };
}
/**
* Process cursor entries into a structured result.
*
* @param entries - Async generator of cursor entries
* @returns Promise resolving to the processed result
*/
async processCursorEntries(entries: AsyncGenerator<CursorEntry>): Promise<any> {
let columns: string[] = [];
let columnTypes: string[] = [];
let rows: any[] = [];
let rowsAffected = 0;
let lastInsertRowid: number | undefined;
for await (const entry of entries) {
switch (entry.type) {
case 'step_begin':
if (entry.cols) {
columns = entry.cols.map(col => col.name);
columnTypes = entry.cols.map(col => col.decltype || '');
}
break;
case 'row':
if (entry.row) {
const decodedRow = entry.row.map(decodeValue);
const rowObject = this.createRowObject(decodedRow, columns);
rows.push(rowObject);
}
break;
case 'step_end':
if (entry.affected_row_count !== undefined) {
rowsAffected = entry.affected_row_count;
}
if (entry.last_insert_rowid) {
lastInsertRowid = parseInt(entry.last_insert_rowid, 10);
}
break;
case 'step_error':
case 'error':
throw new DatabaseError(entry.error?.message || 'SQL execution failed');
}
}
return {
columns,
columnTypes,
rows,
rowsAffected,
lastInsertRowid
};
}
/**
* Create a row object with both array and named property access.
*
* @param values - Array of column values
* @param columns - Array of column names
* @returns Row object with dual access patterns
*/
createRowObject(values: any[], columns: string[]): any {
const row = [...values];
// Add column name properties to the array as non-enumerable
// Only add valid identifier names to avoid conflicts
columns.forEach((column, index) => {
if (column && isValidIdentifier(column)) {
Object.defineProperty(row, column, {
value: values[index],
enumerable: false,
writable: false,
configurable: true
});
}
});
return row;
}
/**
* Execute multiple SQL statements in a batch.
*
* @param statements - Array of SQL statements to execute
* @returns Promise resolving to batch execution results
*/
async batch(statements: string[]): Promise<any> {
const request: CursorRequest = {
baton: this.baton,
batch: {
steps: statements.map(sql => ({
stmt: {
sql,
args: [],
want_rows: false
}
}))
}
};
const { response, entries } = await executeCursor(this.baseUrl, this.config.authToken, request);
this.baton = response.baton;
if (response.base_url) {
this.baseUrl = response.base_url;
}
let totalRowsAffected = 0;
let lastInsertRowid: number | undefined;
for await (const entry of entries) {
switch (entry.type) {
case 'step_end':
if (entry.affected_row_count !== undefined) {
totalRowsAffected += entry.affected_row_count;
}
if (entry.last_insert_rowid) {
lastInsertRowid = parseInt(entry.last_insert_rowid, 10);
}
break;
case 'step_error':
case 'error':
throw new DatabaseError(entry.error?.message || 'Batch execution failed');
}
}
return {
rowsAffected: totalRowsAffected,
lastInsertRowid
};
}
}