@tursodatabase/serverless 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,86 @@
1
+ import { Session } from './session.js';
2
+ import { Statement } from './statement.js';
3
+ /**
4
+ * A connection to a Turso database.
5
+ *
6
+ * Provides methods for executing SQL statements and managing prepared statements.
7
+ * Uses the SQL over HTTP protocol with streaming cursor support for optimal performance.
8
+ */
9
+ export class Connection {
10
+ constructor(config) {
11
+ this.config = config;
12
+ this.session = new Session(config);
13
+ }
14
+ /**
15
+ * Prepare a SQL statement for execution.
16
+ *
17
+ * Each prepared statement gets its own session to avoid conflicts during concurrent execution.
18
+ *
19
+ * @param sql - The SQL statement to prepare
20
+ * @returns A Statement object that can be executed multiple ways
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const stmt = client.prepare("SELECT * FROM users WHERE id = ?");
25
+ * const user = await stmt.get([123]);
26
+ * const allUsers = await stmt.all();
27
+ * ```
28
+ */
29
+ prepare(sql) {
30
+ return new Statement(this.config, sql);
31
+ }
32
+ /**
33
+ * Execute a SQL statement and return all results.
34
+ *
35
+ * @param sql - The SQL statement to execute
36
+ * @param args - Optional array of parameter values
37
+ * @returns Promise resolving to the complete result set
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * const result = await client.execute("SELECT * FROM users");
42
+ * console.log(result.rows);
43
+ * ```
44
+ */
45
+ async execute(sql, args = []) {
46
+ return this.session.execute(sql, args);
47
+ }
48
+ /**
49
+ * Execute multiple SQL statements in a batch.
50
+ *
51
+ * @param statements - Array of SQL statements to execute
52
+ * @param mode - Optional transaction mode (currently unused)
53
+ * @returns Promise resolving to batch execution results
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * await client.batch([
58
+ * "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)",
59
+ * "INSERT INTO users (name) VALUES ('Alice')",
60
+ * "INSERT INTO users (name) VALUES ('Bob')"
61
+ * ]);
62
+ * ```
63
+ */
64
+ async batch(statements, mode) {
65
+ return this.session.batch(statements);
66
+ }
67
+ }
68
+ /**
69
+ * Create a new connection to a Turso database.
70
+ *
71
+ * @param config - Configuration object with database URL and auth token
72
+ * @returns A new Connection instance
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * import { connect } from "@tursodatabase/serverless";
77
+ *
78
+ * const client = connect({
79
+ * url: process.env.TURSO_DATABASE_URL,
80
+ * authToken: process.env.TURSO_AUTH_TOKEN
81
+ * });
82
+ * ```
83
+ */
84
+ export function connect(config) {
85
+ return new Connection(config);
86
+ }
@@ -0,0 +1,2 @@
1
+ export { Connection, connect, type Config } from './connection.js';
2
+ export { Statement } from './statement.js';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ // Turso serverless driver entry point
2
+ export { Connection, connect } from './connection.js';
3
+ export { Statement } from './statement.js';
@@ -0,0 +1,89 @@
1
+ export interface Value {
2
+ type: 'null' | 'integer' | 'float' | 'text' | 'blob';
3
+ value?: string | number;
4
+ base64?: string;
5
+ }
6
+ export interface Column {
7
+ name: string;
8
+ decltype: string;
9
+ }
10
+ export interface ExecuteResult {
11
+ cols: Column[];
12
+ rows: Value[][];
13
+ affected_row_count: number;
14
+ last_insert_rowid?: string;
15
+ }
16
+ export interface ExecuteRequest {
17
+ type: 'execute';
18
+ stmt: {
19
+ sql: string;
20
+ args: Value[];
21
+ named_args: Value[];
22
+ want_rows: boolean;
23
+ };
24
+ }
25
+ export interface BatchStep {
26
+ stmt: {
27
+ sql: string;
28
+ args: Value[];
29
+ want_rows: boolean;
30
+ };
31
+ condition?: {
32
+ type: 'ok';
33
+ step: number;
34
+ };
35
+ }
36
+ export interface BatchRequest {
37
+ type: 'batch';
38
+ batch: {
39
+ steps: BatchStep[];
40
+ };
41
+ }
42
+ export interface PipelineRequest {
43
+ baton: string | null;
44
+ requests: (ExecuteRequest | BatchRequest)[];
45
+ }
46
+ export interface PipelineResponse {
47
+ baton: string | null;
48
+ base_url: string | null;
49
+ results: Array<{
50
+ type: 'ok' | 'error';
51
+ response?: {
52
+ type: 'execute' | 'batch';
53
+ result: ExecuteResult;
54
+ };
55
+ error?: {
56
+ message: string;
57
+ code: string;
58
+ };
59
+ }>;
60
+ }
61
+ export declare function encodeValue(value: any): Value;
62
+ export declare function decodeValue(value: Value): any;
63
+ export interface CursorRequest {
64
+ baton: string | null;
65
+ batch: {
66
+ steps: BatchStep[];
67
+ };
68
+ }
69
+ export interface CursorResponse {
70
+ baton: string | null;
71
+ base_url: string | null;
72
+ }
73
+ export interface CursorEntry {
74
+ type: 'step_begin' | 'step_end' | 'step_error' | 'row' | 'error';
75
+ step?: number;
76
+ cols?: Column[];
77
+ row?: Value[];
78
+ affected_row_count?: number;
79
+ last_insert_rowid?: string;
80
+ error?: {
81
+ message: string;
82
+ code: string;
83
+ };
84
+ }
85
+ export declare function executeCursor(url: string, authToken: string, request: CursorRequest): Promise<{
86
+ response: CursorResponse;
87
+ entries: AsyncGenerator<CursorEntry>;
88
+ }>;
89
+ export declare function executePipeline(url: string, authToken: string, request: PipelineRequest): Promise<PipelineResponse>;
@@ -0,0 +1,128 @@
1
+ export function encodeValue(value) {
2
+ if (value === null || value === undefined) {
3
+ return { type: 'null' };
4
+ }
5
+ if (typeof value === 'number') {
6
+ if (Number.isInteger(value)) {
7
+ return { type: 'integer', value: value.toString() };
8
+ }
9
+ return { type: 'float', value };
10
+ }
11
+ if (typeof value === 'string') {
12
+ return { type: 'text', value };
13
+ }
14
+ if (value instanceof ArrayBuffer || value instanceof Uint8Array) {
15
+ const base64 = btoa(String.fromCharCode(...new Uint8Array(value)));
16
+ return { type: 'blob', base64 };
17
+ }
18
+ return { type: 'text', value: String(value) };
19
+ }
20
+ export function decodeValue(value) {
21
+ switch (value.type) {
22
+ case 'null':
23
+ return null;
24
+ case 'integer':
25
+ return parseInt(value.value, 10);
26
+ case 'float':
27
+ return value.value;
28
+ case 'text':
29
+ return value.value;
30
+ case 'blob':
31
+ if (value.base64) {
32
+ const binaryString = atob(value.base64);
33
+ const bytes = new Uint8Array(binaryString.length);
34
+ for (let i = 0; i < binaryString.length; i++) {
35
+ bytes[i] = binaryString.charCodeAt(i);
36
+ }
37
+ return bytes;
38
+ }
39
+ return null;
40
+ default:
41
+ return null;
42
+ }
43
+ }
44
+ export async function executeCursor(url, authToken, request) {
45
+ const response = await fetch(`${url}/v3/cursor`, {
46
+ method: 'POST',
47
+ headers: {
48
+ 'Content-Type': 'application/json',
49
+ 'Authorization': `Bearer ${authToken}`,
50
+ },
51
+ body: JSON.stringify(request),
52
+ });
53
+ if (!response.ok) {
54
+ let errorMessage = `HTTP error! status: ${response.status}`;
55
+ try {
56
+ const errorBody = await response.text();
57
+ const errorData = JSON.parse(errorBody);
58
+ if (errorData.message) {
59
+ errorMessage = errorData.message;
60
+ }
61
+ }
62
+ catch {
63
+ // If we can't parse the error body, use the default HTTP error message
64
+ }
65
+ throw new Error(errorMessage);
66
+ }
67
+ const reader = response.body?.getReader();
68
+ if (!reader) {
69
+ throw new Error('No response body');
70
+ }
71
+ const decoder = new TextDecoder();
72
+ let buffer = '';
73
+ let isFirstLine = true;
74
+ let cursorResponse;
75
+ async function* parseEntries() {
76
+ try {
77
+ while (true) {
78
+ const { done, value } = await reader.read();
79
+ if (done)
80
+ break;
81
+ buffer += decoder.decode(value, { stream: true });
82
+ let newlineIndex;
83
+ while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
84
+ const line = buffer.slice(0, newlineIndex).trim();
85
+ buffer = buffer.slice(newlineIndex + 1);
86
+ if (line) {
87
+ if (isFirstLine) {
88
+ cursorResponse = JSON.parse(line);
89
+ isFirstLine = false;
90
+ }
91
+ else {
92
+ yield JSON.parse(line);
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+ finally {
99
+ reader.releaseLock();
100
+ }
101
+ }
102
+ const entries = parseEntries();
103
+ // Get the first entry to parse the cursor response
104
+ const firstEntry = await entries.next();
105
+ if (!firstEntry.done) {
106
+ // Put the first entry back
107
+ const generator = (async function* () {
108
+ yield firstEntry.value;
109
+ yield* entries;
110
+ })();
111
+ return { response: cursorResponse, entries: generator };
112
+ }
113
+ return { response: cursorResponse, entries };
114
+ }
115
+ export async function executePipeline(url, authToken, request) {
116
+ const response = await fetch(`${url}/v3/pipeline`, {
117
+ method: 'POST',
118
+ headers: {
119
+ 'Content-Type': 'application/json',
120
+ 'Authorization': `Bearer ${authToken}`,
121
+ },
122
+ body: JSON.stringify(request),
123
+ });
124
+ if (!response.ok) {
125
+ throw new Error(`HTTP error! status: ${response.status}`);
126
+ }
127
+ return response.json();
128
+ }
@@ -0,0 +1,63 @@
1
+ import { type CursorResponse, type CursorEntry } from './protocol.js';
2
+ /**
3
+ * Configuration options for a session.
4
+ */
5
+ export interface SessionConfig {
6
+ /** Database URL */
7
+ url: string;
8
+ /** Authentication token */
9
+ authToken: string;
10
+ }
11
+ /**
12
+ * A database session that manages the connection state and baton.
13
+ *
14
+ * Each session maintains its own connection state and can execute SQL statements
15
+ * independently without interfering with other sessions.
16
+ */
17
+ export declare class Session {
18
+ private config;
19
+ private baton;
20
+ private baseUrl;
21
+ constructor(config: SessionConfig);
22
+ /**
23
+ * Execute a SQL statement and return all results.
24
+ *
25
+ * @param sql - The SQL statement to execute
26
+ * @param args - Optional array of parameter values
27
+ * @returns Promise resolving to the complete result set
28
+ */
29
+ execute(sql: string, args?: any[]): Promise<any>;
30
+ /**
31
+ * Execute a SQL statement and return the raw response and entries.
32
+ *
33
+ * @param sql - The SQL statement to execute
34
+ * @param args - Optional array of parameter values
35
+ * @returns Promise resolving to the raw response and cursor entries
36
+ */
37
+ executeRaw(sql: string, args?: any[]): Promise<{
38
+ response: CursorResponse;
39
+ entries: AsyncGenerator<CursorEntry>;
40
+ }>;
41
+ /**
42
+ * Process cursor entries into a structured result.
43
+ *
44
+ * @param entries - Async generator of cursor entries
45
+ * @returns Promise resolving to the processed result
46
+ */
47
+ processCursorEntries(entries: AsyncGenerator<CursorEntry>): Promise<any>;
48
+ /**
49
+ * Create a row object with both array and named property access.
50
+ *
51
+ * @param values - Array of column values
52
+ * @param columns - Array of column names
53
+ * @returns Row object with dual access patterns
54
+ */
55
+ createRowObject(values: any[], columns: string[]): any;
56
+ /**
57
+ * Execute multiple SQL statements in a batch.
58
+ *
59
+ * @param statements - Array of SQL statements to execute
60
+ * @returns Promise resolving to batch execution results
61
+ */
62
+ batch(statements: string[]): Promise<any>;
63
+ }
@@ -0,0 +1,176 @@
1
+ import { executeCursor, encodeValue, decodeValue } from './protocol.js';
2
+ function normalizeUrl(url) {
3
+ return url.replace(/^libsql:\/\//, 'https://');
4
+ }
5
+ function isValidIdentifier(str) {
6
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(str);
7
+ }
8
+ /**
9
+ * A database session that manages the connection state and baton.
10
+ *
11
+ * Each session maintains its own connection state and can execute SQL statements
12
+ * independently without interfering with other sessions.
13
+ */
14
+ export class Session {
15
+ constructor(config) {
16
+ this.baton = null;
17
+ this.config = config;
18
+ this.baseUrl = normalizeUrl(config.url);
19
+ }
20
+ /**
21
+ * Execute a SQL statement and return all results.
22
+ *
23
+ * @param sql - The SQL statement to execute
24
+ * @param args - Optional array of parameter values
25
+ * @returns Promise resolving to the complete result set
26
+ */
27
+ async execute(sql, args = []) {
28
+ const { response, entries } = await this.executeRaw(sql, args);
29
+ const result = await this.processCursorEntries(entries);
30
+ return result;
31
+ }
32
+ /**
33
+ * Execute a SQL statement and return the raw response and entries.
34
+ *
35
+ * @param sql - The SQL statement to execute
36
+ * @param args - Optional array of parameter values
37
+ * @returns Promise resolving to the raw response and cursor entries
38
+ */
39
+ async executeRaw(sql, args = []) {
40
+ const request = {
41
+ baton: this.baton,
42
+ batch: {
43
+ steps: [{
44
+ stmt: {
45
+ sql,
46
+ args: args.map(encodeValue),
47
+ want_rows: true
48
+ }
49
+ }]
50
+ }
51
+ };
52
+ const { response, entries } = await executeCursor(this.baseUrl, this.config.authToken, request);
53
+ this.baton = response.baton;
54
+ if (response.base_url) {
55
+ this.baseUrl = response.base_url;
56
+ }
57
+ return { response, entries };
58
+ }
59
+ /**
60
+ * Process cursor entries into a structured result.
61
+ *
62
+ * @param entries - Async generator of cursor entries
63
+ * @returns Promise resolving to the processed result
64
+ */
65
+ async processCursorEntries(entries) {
66
+ let columns = [];
67
+ let columnTypes = [];
68
+ let rows = [];
69
+ let rowsAffected = 0;
70
+ let lastInsertRowid;
71
+ for await (const entry of entries) {
72
+ switch (entry.type) {
73
+ case 'step_begin':
74
+ if (entry.cols) {
75
+ columns = entry.cols.map(col => col.name);
76
+ columnTypes = entry.cols.map(col => col.decltype || '');
77
+ }
78
+ break;
79
+ case 'row':
80
+ if (entry.row) {
81
+ const decodedRow = entry.row.map(decodeValue);
82
+ const rowObject = this.createRowObject(decodedRow, columns);
83
+ rows.push(rowObject);
84
+ }
85
+ break;
86
+ case 'step_end':
87
+ if (entry.affected_row_count !== undefined) {
88
+ rowsAffected = entry.affected_row_count;
89
+ }
90
+ if (entry.last_insert_rowid) {
91
+ lastInsertRowid = parseInt(entry.last_insert_rowid, 10);
92
+ }
93
+ break;
94
+ case 'step_error':
95
+ case 'error':
96
+ throw new Error(entry.error?.message || 'SQL execution failed');
97
+ }
98
+ }
99
+ return {
100
+ columns,
101
+ columnTypes,
102
+ rows,
103
+ rowsAffected,
104
+ lastInsertRowid
105
+ };
106
+ }
107
+ /**
108
+ * Create a row object with both array and named property access.
109
+ *
110
+ * @param values - Array of column values
111
+ * @param columns - Array of column names
112
+ * @returns Row object with dual access patterns
113
+ */
114
+ createRowObject(values, columns) {
115
+ const row = [...values];
116
+ // Add column name properties to the array as non-enumerable
117
+ // Only add valid identifier names to avoid conflicts
118
+ columns.forEach((column, index) => {
119
+ if (column && isValidIdentifier(column)) {
120
+ Object.defineProperty(row, column, {
121
+ value: values[index],
122
+ enumerable: false,
123
+ writable: false,
124
+ configurable: true
125
+ });
126
+ }
127
+ });
128
+ return row;
129
+ }
130
+ /**
131
+ * Execute multiple SQL statements in a batch.
132
+ *
133
+ * @param statements - Array of SQL statements to execute
134
+ * @returns Promise resolving to batch execution results
135
+ */
136
+ async batch(statements) {
137
+ const request = {
138
+ baton: this.baton,
139
+ batch: {
140
+ steps: statements.map(sql => ({
141
+ stmt: {
142
+ sql,
143
+ args: [],
144
+ want_rows: false
145
+ }
146
+ }))
147
+ }
148
+ };
149
+ const { response, entries } = await executeCursor(this.baseUrl, this.config.authToken, request);
150
+ this.baton = response.baton;
151
+ if (response.base_url) {
152
+ this.baseUrl = response.base_url;
153
+ }
154
+ let totalRowsAffected = 0;
155
+ let lastInsertRowid;
156
+ for await (const entry of entries) {
157
+ switch (entry.type) {
158
+ case 'step_end':
159
+ if (entry.affected_row_count !== undefined) {
160
+ totalRowsAffected += entry.affected_row_count;
161
+ }
162
+ if (entry.last_insert_rowid) {
163
+ lastInsertRowid = parseInt(entry.last_insert_rowid, 10);
164
+ }
165
+ break;
166
+ case 'step_error':
167
+ case 'error':
168
+ throw new Error(entry.error?.message || 'Batch execution failed');
169
+ }
170
+ }
171
+ return {
172
+ rowsAffected: totalRowsAffected,
173
+ lastInsertRowid
174
+ };
175
+ }
176
+ }
@@ -0,0 +1,64 @@
1
+ import { type SessionConfig } from './session.js';
2
+ /**
3
+ * A prepared SQL statement that can be executed in multiple ways.
4
+ *
5
+ * Each statement has its own session to avoid conflicts during concurrent execution.
6
+ * Provides three execution modes:
7
+ * - `get(args?)`: Returns the first row or null
8
+ * - `all(args?)`: Returns all rows as an array
9
+ * - `iterate(args?)`: Returns an async iterator for streaming results
10
+ */
11
+ export declare class Statement {
12
+ private session;
13
+ private sql;
14
+ constructor(sessionConfig: SessionConfig, sql: string);
15
+ /**
16
+ * Execute the statement and return the first row.
17
+ *
18
+ * @param args - Optional array of parameter values for the SQL statement
19
+ * @returns Promise resolving to the first row or null if no results
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * const stmt = client.prepare("SELECT * FROM users WHERE id = ?");
24
+ * const user = await stmt.get([123]);
25
+ * if (user) {
26
+ * console.log(user.name);
27
+ * }
28
+ * ```
29
+ */
30
+ get(args?: any[]): Promise<any>;
31
+ /**
32
+ * Execute the statement and return all rows.
33
+ *
34
+ * @param args - Optional array of parameter values for the SQL statement
35
+ * @returns Promise resolving to an array of all result rows
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * const stmt = client.prepare("SELECT * FROM users WHERE active = ?");
40
+ * const activeUsers = await stmt.all([true]);
41
+ * console.log(`Found ${activeUsers.length} active users`);
42
+ * ```
43
+ */
44
+ all(args?: any[]): Promise<any[]>;
45
+ /**
46
+ * Execute the statement and return an async iterator for streaming results.
47
+ *
48
+ * This method provides memory-efficient processing of large result sets
49
+ * by streaming rows one at a time instead of loading everything into memory.
50
+ *
51
+ * @param args - Optional array of parameter values for the SQL statement
52
+ * @returns AsyncGenerator that yields individual rows
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * const stmt = client.prepare("SELECT * FROM large_table WHERE category = ?");
57
+ * for await (const row of stmt.iterate(['electronics'])) {
58
+ * // Process each row individually
59
+ * console.log(row.id, row.name);
60
+ * }
61
+ * ```
62
+ */
63
+ iterate(args?: any[]): AsyncGenerator<any>;
64
+ }