mirror of
https://github.com/aljazceru/turso.git
synced 2025-12-30 06:24:21 +01:00
337 lines
11 KiB
TypeScript
337 lines
11 KiB
TypeScript
import { Session, type SessionConfig } from './session.js';
|
|
import { DatabaseError } from './error.js';
|
|
|
|
/**
|
|
* Configuration options for creating a libSQL-compatible client.
|
|
*
|
|
* @remarks
|
|
* This interface matches the libSQL client configuration but only `url` and `authToken`
|
|
* are supported in the serverless compatibility layer. Other options will throw validation errors.
|
|
*/
|
|
export interface Config {
|
|
/** Database URL (required) */
|
|
url: string;
|
|
/** Authentication token for the database */
|
|
authToken?: string;
|
|
/** @deprecated Database encryption key - not supported in serverless mode */
|
|
encryptionKey?: string;
|
|
/** @deprecated Sync server URL - not supported in serverless mode */
|
|
syncUrl?: string;
|
|
/** @deprecated Sync frequency in seconds - not supported in serverless mode */
|
|
syncInterval?: number;
|
|
/** @deprecated Consistency mode - not supported in serverless mode */
|
|
readYourWrites?: boolean;
|
|
/** @deprecated Offline mode support - not supported in serverless mode */
|
|
offline?: boolean;
|
|
/** @deprecated TLS settings - not supported in serverless mode */
|
|
tls?: boolean;
|
|
/** @deprecated Integer handling mode - not supported in serverless mode */
|
|
intMode?: "number" | "bigint" | "string";
|
|
/** @deprecated Custom fetch implementation - not supported in serverless mode */
|
|
fetch?: Function;
|
|
/** @deprecated Concurrent request limit - not supported in serverless mode */
|
|
concurrency?: number;
|
|
}
|
|
|
|
/** Input value types accepted by libSQL statements */
|
|
export type InValue = null | string | number | bigint | ArrayBuffer | boolean | Uint8Array | Date;
|
|
|
|
/** Input arguments - either positional array or named object */
|
|
export type InArgs = Array<InValue> | Record<string, InValue>;
|
|
|
|
/** Input statement - either SQL string or object with sql and args */
|
|
export type InStatement = { sql: string; args?: InArgs } | string;
|
|
|
|
/** Transaction execution modes */
|
|
export type TransactionMode = "write" | "read" | "deferred";
|
|
|
|
/**
|
|
* A result row that can be accessed both as an array and as an object.
|
|
* Supports both numeric indexing (row[0]) and column name access (row.column_name).
|
|
*/
|
|
export interface Row {
|
|
length: number;
|
|
[index: number]: InValue;
|
|
[name: string]: InValue;
|
|
}
|
|
|
|
/**
|
|
* Result set returned from SQL statement execution.
|
|
*/
|
|
export interface ResultSet {
|
|
/** Column names in the result set */
|
|
columns: Array<string>;
|
|
/** Column type information */
|
|
columnTypes: Array<string>;
|
|
/** Result rows */
|
|
rows: Array<Row>;
|
|
/** Number of rows affected by the statement */
|
|
rowsAffected: number;
|
|
/** ID of the last inserted row (for INSERT statements) */
|
|
lastInsertRowid: bigint | undefined;
|
|
/** Convert result set to JSON */
|
|
toJSON(): any;
|
|
}
|
|
|
|
/**
|
|
* libSQL-compatible error class with error codes.
|
|
*/
|
|
export class LibsqlError extends Error {
|
|
/** Machine-readable error code */
|
|
code: string;
|
|
/** Raw numeric error code (if available) */
|
|
rawCode?: number;
|
|
|
|
constructor(message: string, code: string, rawCode?: number) {
|
|
super(message);
|
|
this.name = 'LibsqlError';
|
|
this.code = code;
|
|
this.rawCode = rawCode;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Interactive transaction interface (not implemented in serverless mode).
|
|
*
|
|
* @remarks
|
|
* Transactions are not supported in the serverless compatibility layer.
|
|
* Calling transaction() will throw a LibsqlError.
|
|
*/
|
|
export interface Transaction {
|
|
execute(stmt: InStatement): Promise<ResultSet>;
|
|
batch(stmts: Array<InStatement>): Promise<Array<ResultSet>>;
|
|
executeMultiple(sql: string): Promise<void>;
|
|
commit(): Promise<void>;
|
|
rollback(): Promise<void>;
|
|
close(): void;
|
|
closed: boolean;
|
|
}
|
|
|
|
/**
|
|
* libSQL-compatible client interface.
|
|
*
|
|
* This interface matches the standard libSQL client API for drop-in compatibility.
|
|
* Some methods are not implemented in the serverless compatibility layer.
|
|
*/
|
|
export interface Client {
|
|
execute(stmt: InStatement): Promise<ResultSet>;
|
|
execute(sql: string, args?: InArgs): Promise<ResultSet>;
|
|
batch(stmts: Array<InStatement>, mode?: TransactionMode): Promise<Array<ResultSet>>;
|
|
migrate(stmts: Array<InStatement>): Promise<Array<ResultSet>>;
|
|
transaction(mode?: TransactionMode): Promise<Transaction>;
|
|
executeMultiple(sql: string): Promise<void>;
|
|
sync(): Promise<any>;
|
|
close(): void;
|
|
closed: boolean;
|
|
protocol: string;
|
|
}
|
|
|
|
class LibSQLClient implements Client {
|
|
private session: Session;
|
|
private _closed = false;
|
|
private _defaultSafeIntegers = false;
|
|
|
|
constructor(config: Config) {
|
|
this.validateConfig(config);
|
|
|
|
const sessionConfig: SessionConfig = {
|
|
url: config.url,
|
|
authToken: config.authToken || ''
|
|
};
|
|
this.session = new Session(sessionConfig);
|
|
}
|
|
|
|
private validateConfig(config: Config): void {
|
|
// Check for unsupported config options
|
|
const unsupportedOptions: Array<{ key: keyof Config; value: any }> = [];
|
|
|
|
if (config.encryptionKey !== undefined) {
|
|
unsupportedOptions.push({ key: 'encryptionKey', value: config.encryptionKey });
|
|
}
|
|
if (config.syncUrl !== undefined) {
|
|
unsupportedOptions.push({ key: 'syncUrl', value: config.syncUrl });
|
|
}
|
|
if (config.syncInterval !== undefined) {
|
|
unsupportedOptions.push({ key: 'syncInterval', value: config.syncInterval });
|
|
}
|
|
if (config.readYourWrites !== undefined) {
|
|
unsupportedOptions.push({ key: 'readYourWrites', value: config.readYourWrites });
|
|
}
|
|
if (config.offline !== undefined) {
|
|
unsupportedOptions.push({ key: 'offline', value: config.offline });
|
|
}
|
|
if (config.tls !== undefined) {
|
|
unsupportedOptions.push({ key: 'tls', value: config.tls });
|
|
}
|
|
if (config.intMode !== undefined) {
|
|
unsupportedOptions.push({ key: 'intMode', value: config.intMode });
|
|
}
|
|
if (config.fetch !== undefined) {
|
|
unsupportedOptions.push({ key: 'fetch', value: config.fetch });
|
|
}
|
|
if (config.concurrency !== undefined) {
|
|
unsupportedOptions.push({ key: 'concurrency', value: config.concurrency });
|
|
}
|
|
|
|
if (unsupportedOptions.length > 0) {
|
|
const optionsList = unsupportedOptions.map(opt => `'${opt.key}'`).join(', ');
|
|
throw new LibsqlError(
|
|
`Unsupported configuration options: ${optionsList}. Only 'url' and 'authToken' are supported in the serverless compatibility layer.`,
|
|
"UNSUPPORTED_CONFIG"
|
|
);
|
|
}
|
|
|
|
// Validate required options
|
|
if (!config.url) {
|
|
throw new LibsqlError("Missing required 'url' configuration option", "MISSING_URL");
|
|
}
|
|
}
|
|
|
|
get closed(): boolean {
|
|
return this._closed;
|
|
}
|
|
|
|
get protocol(): string {
|
|
return "http";
|
|
}
|
|
|
|
private normalizeStatement(stmt: InStatement): { sql: string; args: any[] } {
|
|
if (typeof stmt === 'string') {
|
|
return { sql: stmt, args: [] };
|
|
}
|
|
|
|
const args = stmt.args || [];
|
|
if (Array.isArray(args)) {
|
|
return { sql: stmt.sql, args };
|
|
}
|
|
|
|
// Convert named args to positional args (simplified)
|
|
return { sql: stmt.sql, args: Object.values(args) };
|
|
}
|
|
|
|
private convertResult(result: any): ResultSet {
|
|
const resultSet: ResultSet = {
|
|
columns: result.columns || [],
|
|
columnTypes: result.columnTypes || [],
|
|
rows: result.rows || [],
|
|
rowsAffected: result.rowsAffected || 0,
|
|
lastInsertRowid: result.lastInsertRowid ? BigInt(result.lastInsertRowid) : undefined,
|
|
toJSON() {
|
|
return {
|
|
columns: this.columns,
|
|
columnTypes: this.columnTypes,
|
|
rows: this.rows,
|
|
rowsAffected: this.rowsAffected,
|
|
lastInsertRowid: this.lastInsertRowid?.toString()
|
|
};
|
|
}
|
|
};
|
|
|
|
return resultSet;
|
|
}
|
|
|
|
async execute(stmt: InStatement): Promise<ResultSet>;
|
|
async execute(sql: string, args?: InArgs): Promise<ResultSet>;
|
|
async execute(stmtOrSql: InStatement | string, args?: InArgs): Promise<ResultSet> {
|
|
try {
|
|
if (this._closed) {
|
|
throw new LibsqlError("Client is closed", "CLIENT_CLOSED");
|
|
}
|
|
|
|
let normalizedStmt: { sql: string; args: any[] };
|
|
|
|
if (typeof stmtOrSql === 'string') {
|
|
const normalizedArgs = args ? (Array.isArray(args) ? args : Object.values(args)) : [];
|
|
normalizedStmt = { sql: stmtOrSql, args: normalizedArgs };
|
|
} else {
|
|
normalizedStmt = this.normalizeStatement(stmtOrSql);
|
|
}
|
|
|
|
const result = await this.session.execute(normalizedStmt.sql, normalizedStmt.args, this._defaultSafeIntegers);
|
|
return this.convertResult(result);
|
|
} catch (error: any) {
|
|
throw new LibsqlError(error.message, "EXECUTE_ERROR");
|
|
}
|
|
}
|
|
|
|
async batch(stmts: Array<InStatement>, mode?: TransactionMode): Promise<Array<ResultSet>> {
|
|
try {
|
|
if (this._closed) {
|
|
throw new LibsqlError("Client is closed", "CLIENT_CLOSED");
|
|
}
|
|
|
|
const sqlStatements = stmts.map(stmt => {
|
|
const normalized = this.normalizeStatement(stmt);
|
|
return normalized.sql; // For now, ignore args in batch
|
|
});
|
|
|
|
const result = await this.session.batch(sqlStatements);
|
|
|
|
// Return array of result sets (simplified - actual implementation would be more complex)
|
|
return [this.convertResult(result)];
|
|
} catch (error: any) {
|
|
throw new LibsqlError(error.message, "BATCH_ERROR");
|
|
}
|
|
}
|
|
|
|
async migrate(stmts: Array<InStatement>): Promise<Array<ResultSet>> {
|
|
// For now, just call batch - in a real implementation this would disable foreign keys
|
|
return this.batch(stmts, "write");
|
|
}
|
|
|
|
async transaction(mode?: TransactionMode): Promise<Transaction> {
|
|
throw new LibsqlError("Transactions not implemented", "NOT_IMPLEMENTED");
|
|
}
|
|
|
|
async executeMultiple(sql: string): Promise<void> {
|
|
try {
|
|
if (this._closed) {
|
|
throw new LibsqlError("Client is closed", "CLIENT_CLOSED");
|
|
}
|
|
|
|
await this.session.sequence(sql);
|
|
} catch (error: any) {
|
|
throw new LibsqlError(error.message, "EXECUTE_MULTIPLE_ERROR");
|
|
}
|
|
}
|
|
|
|
async sync(): Promise<any> {
|
|
throw new LibsqlError("Sync not supported for remote databases", "NOT_SUPPORTED");
|
|
}
|
|
|
|
close(): void {
|
|
this._closed = true;
|
|
// Note: The libSQL client interface expects synchronous close,
|
|
// but our underlying session needs async close. We'll fire and forget.
|
|
this.session.close().catch(error => {
|
|
console.error('Error closing session:', error);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a libSQL-compatible client for Turso database access.
|
|
*
|
|
* This function provides compatibility with the standard libSQL client API
|
|
* while using the Turso serverless driver under the hood.
|
|
*
|
|
* @param config - Configuration object (only url and authToken are supported)
|
|
* @returns A Client instance compatible with libSQL API
|
|
* @throws LibsqlError if unsupported configuration options are provided
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { createClient } from "@tursodatabase/serverless/compat";
|
|
*
|
|
* const client = createClient({
|
|
* url: process.env.TURSO_DATABASE_URL,
|
|
* authToken: process.env.TURSO_AUTH_TOKEN
|
|
* });
|
|
*
|
|
* const result = await client.execute("SELECT * FROM users");
|
|
* console.log(result.rows);
|
|
* ```
|
|
*/
|
|
export function createClient(config: Config): Client {
|
|
return new LibSQLClient(config);
|
|
} |