@tursodatabase/serverless 0.1.3 → 0.2.1

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.
package/README.md CHANGED
@@ -16,7 +16,7 @@
16
16
 
17
17
  A serverless database driver for Turso Cloud, using only `fetch()`. Connect to your database from serverless and edge functions, such as Cloudflare Workers and Vercel.
18
18
 
19
- > **📝 Note:** This driver is experimental and, therefore, subject to change at any time.
19
+ > **⚠️ Warning:** This software is in BETA. It may still contain bugs and unexpected behavior. Use caution with production data and ensure you have backups.
20
20
 
21
21
  ## Installation
22
22
 
@@ -109,4 +109,4 @@ This project is licensed under the [MIT license](../../LICENSE.md).
109
109
 
110
110
  - [GitHub Issues](https://github.com/tursodatabase/turso/issues)
111
111
  - [Documentation](https://docs.turso.tech)
112
- - [Discord Community](https://tur.so/discord)
112
+ - [Discord Community](https://tur.so/discord)
package/dist/compat.d.ts CHANGED
@@ -2,16 +2,22 @@
2
2
  * Configuration options for creating a libSQL-compatible client.
3
3
  *
4
4
  * @remarks
5
- * This interface matches the libSQL client configuration but only `url` and `authToken`
6
- * are supported in the serverless compatibility layer. Other options will throw validation errors.
5
+ * This interface matches the libSQL client configuration. The `url`, `authToken`, and
6
+ * `remoteEncryptionKey` options are supported in the serverless compatibility layer.
7
+ * Other options will throw validation errors.
7
8
  */
8
9
  export interface Config {
9
10
  /** Database URL (required) */
10
11
  url: string;
11
12
  /** Authentication token for the database */
12
13
  authToken?: string;
13
- /** @deprecated Database encryption key - not supported in serverless mode */
14
+ /** @deprecated Local database encryption key - not supported in serverless mode */
14
15
  encryptionKey?: string;
16
+ /**
17
+ * Encryption key for the remote database (base64 encoded)
18
+ * to enable access to encrypted Turso Cloud databases.
19
+ */
20
+ remoteEncryptionKey?: string;
15
21
  /** @deprecated Sync server URL - not supported in serverless mode */
16
22
  syncUrl?: string;
17
23
  /** @deprecated Sync frequency in seconds - not supported in serverless mode */
package/dist/compat.js CHANGED
@@ -13,10 +13,12 @@ export class LibsqlError extends Error {
13
13
  class LibSQLClient {
14
14
  constructor(config) {
15
15
  this._closed = false;
16
+ this._defaultSafeIntegers = false;
16
17
  this.validateConfig(config);
17
18
  const sessionConfig = {
18
19
  url: config.url,
19
- authToken: config.authToken || ''
20
+ authToken: config.authToken || '',
21
+ remoteEncryptionKey: config.remoteEncryptionKey
20
22
  };
21
23
  this.session = new Session(sessionConfig);
22
24
  }
@@ -52,7 +54,7 @@ class LibSQLClient {
52
54
  }
53
55
  if (unsupportedOptions.length > 0) {
54
56
  const optionsList = unsupportedOptions.map(opt => `'${opt.key}'`).join(', ');
55
- throw new LibsqlError(`Unsupported configuration options: ${optionsList}. Only 'url' and 'authToken' are supported in the serverless compatibility layer.`, "UNSUPPORTED_CONFIG");
57
+ throw new LibsqlError(`Unsupported configuration options: ${optionsList}. Only 'url', 'authToken', and 'remoteEncryptionKey' are supported in the serverless compatibility layer.`, "UNSUPPORTED_CONFIG");
56
58
  }
57
59
  // Validate required options
58
60
  if (!config.url) {
@@ -108,15 +110,8 @@ class LibSQLClient {
108
110
  else {
109
111
  normalizedStmt = this.normalizeStatement(stmtOrSql);
110
112
  }
111
- await this.session.sequence(normalizedStmt.sql);
112
- // Return empty result set for sequence execution
113
- return this.convertResult({
114
- columns: [],
115
- columnTypes: [],
116
- rows: [],
117
- rowsAffected: 0,
118
- lastInsertRowid: undefined
119
- });
113
+ const result = await this.session.execute(normalizedStmt.sql, normalizedStmt.args, this._defaultSafeIntegers);
114
+ return this.convertResult(result);
120
115
  }
121
116
  catch (error) {
122
117
  throw new LibsqlError(error.message, "EXECUTE_ERROR");
@@ -15,23 +15,44 @@ export declare class Connection {
15
15
  private config;
16
16
  private session;
17
17
  private isOpen;
18
+ private defaultSafeIntegerMode;
19
+ private _inTransaction;
18
20
  constructor(config: Config);
21
+ /**
22
+ * Whether the database is currently in a transaction.
23
+ */
24
+ get inTransaction(): boolean;
19
25
  /**
20
26
  * Prepare a SQL statement for execution.
21
27
  *
22
28
  * Each prepared statement gets its own session to avoid conflicts during concurrent execution.
29
+ * This method fetches column metadata using the describe functionality.
23
30
  *
24
31
  * @param sql - The SQL statement to prepare
25
- * @returns A Statement object that can be executed multiple ways
32
+ * @returns A Promise that resolves to a Statement object with column metadata
26
33
  *
27
34
  * @example
28
35
  * ```typescript
29
- * const stmt = client.prepare("SELECT * FROM users WHERE id = ?");
36
+ * const stmt = await client.prepare("SELECT * FROM users WHERE id = ?");
37
+ * const columns = stmt.columns();
30
38
  * const user = await stmt.get([123]);
31
- * const allUsers = await stmt.all();
32
39
  * ```
33
40
  */
34
- prepare(sql: string): Statement;
41
+ prepare(sql: string): Promise<Statement>;
42
+ /**
43
+ * Execute a SQL statement and return all results.
44
+ *
45
+ * @param sql - The SQL statement to execute
46
+ * @param args - Optional array of parameter values
47
+ * @returns Promise resolving to the complete result set
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * const result = await client.execute("SELECT * FROM users WHERE id = ?", [123]);
52
+ * console.log(result.rows);
53
+ * ```
54
+ */
55
+ execute(sql: string, args?: any[]): Promise<any>;
35
56
  /**
36
57
  * Execute a SQL statement and return all results.
37
58
  *
@@ -40,7 +61,7 @@ export declare class Connection {
40
61
  *
41
62
  * @example
42
63
  * ```typescript
43
- * const result = await client.execute("SELECT * FROM users");
64
+ * const result = await client.exec("SELECT * FROM users");
44
65
  * console.log(result.rows);
45
66
  * ```
46
67
  */
@@ -69,12 +90,38 @@ export declare class Connection {
69
90
  * @returns Promise resolving to the result of the pragma
70
91
  */
71
92
  pragma(pragma: string): Promise<any>;
93
+ /**
94
+ * Sets the default safe integers mode for all statements from this connection.
95
+ *
96
+ * @param toggle - Whether to use safe integers by default.
97
+ */
98
+ defaultSafeIntegers(toggle?: boolean): void;
99
+ /**
100
+ * Returns a function that executes the given function in a transaction.
101
+ *
102
+ * @param fn - The function to wrap in a transaction
103
+ * @returns A function that will execute fn within a transaction
104
+ *
105
+ * @example
106
+ * ```typescript
107
+ * const insert = await client.prepare("INSERT INTO users (name) VALUES (?)");
108
+ * const insertMany = client.transaction((users) => {
109
+ * for (const user of users) {
110
+ * insert.run([user]);
111
+ * }
112
+ * });
113
+ *
114
+ * await insertMany(['Alice', 'Bob', 'Charlie']);
115
+ * ```
116
+ */
117
+ transaction(fn: (...args: any[]) => any): any;
72
118
  /**
73
119
  * Close the connection.
74
120
  *
75
121
  * This sends a close request to the server to properly clean up the stream.
76
122
  */
77
123
  close(): Promise<void>;
124
+ reconnect(): Promise<void>;
78
125
  }
79
126
  /**
80
127
  * Create a new connection to a Turso database.
@@ -9,32 +9,73 @@ import { Statement } from './statement.js';
9
9
  export class Connection {
10
10
  constructor(config) {
11
11
  this.isOpen = true;
12
+ this.defaultSafeIntegerMode = false;
13
+ this._inTransaction = false;
12
14
  if (!config.url) {
13
15
  throw new Error("invalid config: url is required");
14
16
  }
15
17
  this.config = config;
16
18
  this.session = new Session(config);
19
+ // Define inTransaction property
20
+ Object.defineProperty(this, 'inTransaction', {
21
+ get: () => this._inTransaction,
22
+ enumerable: true
23
+ });
24
+ }
25
+ /**
26
+ * Whether the database is currently in a transaction.
27
+ */
28
+ get inTransaction() {
29
+ return this._inTransaction;
17
30
  }
18
31
  /**
19
32
  * Prepare a SQL statement for execution.
20
33
  *
21
34
  * Each prepared statement gets its own session to avoid conflicts during concurrent execution.
35
+ * This method fetches column metadata using the describe functionality.
22
36
  *
23
37
  * @param sql - The SQL statement to prepare
24
- * @returns A Statement object that can be executed multiple ways
38
+ * @returns A Promise that resolves to a Statement object with column metadata
25
39
  *
26
40
  * @example
27
41
  * ```typescript
28
- * const stmt = client.prepare("SELECT * FROM users WHERE id = ?");
42
+ * const stmt = await client.prepare("SELECT * FROM users WHERE id = ?");
43
+ * const columns = stmt.columns();
29
44
  * const user = await stmt.get([123]);
30
- * const allUsers = await stmt.all();
31
45
  * ```
32
46
  */
33
- prepare(sql) {
47
+ async prepare(sql) {
48
+ if (!this.isOpen) {
49
+ throw new TypeError("The database connection is not open");
50
+ }
51
+ // Create a session to get column metadata via describe
52
+ const session = new Session(this.config);
53
+ const description = await session.describe(sql);
54
+ await session.close();
55
+ const stmt = new Statement(this.config, sql, description.cols);
56
+ if (this.defaultSafeIntegerMode) {
57
+ stmt.safeIntegers(true);
58
+ }
59
+ return stmt;
60
+ }
61
+ /**
62
+ * Execute a SQL statement and return all results.
63
+ *
64
+ * @param sql - The SQL statement to execute
65
+ * @param args - Optional array of parameter values
66
+ * @returns Promise resolving to the complete result set
67
+ *
68
+ * @example
69
+ * ```typescript
70
+ * const result = await client.execute("SELECT * FROM users WHERE id = ?", [123]);
71
+ * console.log(result.rows);
72
+ * ```
73
+ */
74
+ async execute(sql, args) {
34
75
  if (!this.isOpen) {
35
76
  throw new TypeError("The database connection is not open");
36
77
  }
37
- return new Statement(this.config, sql);
78
+ return this.session.execute(sql, args || [], this.defaultSafeIntegerMode);
38
79
  }
39
80
  /**
40
81
  * Execute a SQL statement and return all results.
@@ -44,7 +85,7 @@ export class Connection {
44
85
  *
45
86
  * @example
46
87
  * ```typescript
47
- * const result = await client.execute("SELECT * FROM users");
88
+ * const result = await client.exec("SELECT * FROM users");
48
89
  * console.log(result.rows);
49
90
  * ```
50
91
  */
@@ -86,6 +127,67 @@ export class Connection {
86
127
  const sql = `PRAGMA ${pragma}`;
87
128
  return this.session.execute(sql);
88
129
  }
130
+ /**
131
+ * Sets the default safe integers mode for all statements from this connection.
132
+ *
133
+ * @param toggle - Whether to use safe integers by default.
134
+ */
135
+ defaultSafeIntegers(toggle) {
136
+ this.defaultSafeIntegerMode = toggle === false ? false : true;
137
+ }
138
+ /**
139
+ * Returns a function that executes the given function in a transaction.
140
+ *
141
+ * @param fn - The function to wrap in a transaction
142
+ * @returns A function that will execute fn within a transaction
143
+ *
144
+ * @example
145
+ * ```typescript
146
+ * const insert = await client.prepare("INSERT INTO users (name) VALUES (?)");
147
+ * const insertMany = client.transaction((users) => {
148
+ * for (const user of users) {
149
+ * insert.run([user]);
150
+ * }
151
+ * });
152
+ *
153
+ * await insertMany(['Alice', 'Bob', 'Charlie']);
154
+ * ```
155
+ */
156
+ transaction(fn) {
157
+ if (typeof fn !== "function") {
158
+ throw new TypeError("Expected first argument to be a function");
159
+ }
160
+ const db = this;
161
+ const wrapTxn = (mode) => {
162
+ return async (...bindParameters) => {
163
+ await db.exec("BEGIN " + mode);
164
+ db._inTransaction = true;
165
+ try {
166
+ const result = await fn(...bindParameters);
167
+ await db.exec("COMMIT");
168
+ db._inTransaction = false;
169
+ return result;
170
+ }
171
+ catch (err) {
172
+ await db.exec("ROLLBACK");
173
+ db._inTransaction = false;
174
+ throw err;
175
+ }
176
+ };
177
+ };
178
+ const properties = {
179
+ default: { value: wrapTxn("") },
180
+ deferred: { value: wrapTxn("DEFERRED") },
181
+ immediate: { value: wrapTxn("IMMEDIATE") },
182
+ exclusive: { value: wrapTxn("EXCLUSIVE") },
183
+ database: { value: this, enumerable: true },
184
+ };
185
+ Object.defineProperties(properties.default.value, properties);
186
+ Object.defineProperties(properties.deferred.value, properties);
187
+ Object.defineProperties(properties.immediate.value, properties);
188
+ Object.defineProperties(properties.exclusive.value, properties);
189
+ return properties.default.value;
190
+ }
89
191
  /**
90
192
  * Close the connection.
91
193
  *
@@ -95,6 +197,17 @@ export class Connection {
95
197
  this.isOpen = false;
96
198
  await this.session.close();
97
199
  }
200
+ async reconnect() {
201
+ try {
202
+ if (this.isOpen) {
203
+ await this.close();
204
+ }
205
+ }
206
+ finally {
207
+ this.session = new Session(this.config);
208
+ this.isOpen = true;
209
+ }
210
+ }
98
211
  }
99
212
  /**
100
213
  * Create a new connection to a Turso database.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { Connection, connect, type Config } from './connection.js';
2
2
  export { Statement } from './statement.js';
3
3
  export { DatabaseError } from './error.js';
4
+ export { type Column, ENCRYPTION_KEY_HEADER } from './protocol.js';
package/dist/index.js CHANGED
@@ -2,3 +2,4 @@
2
2
  export { Connection, connect } from './connection.js';
3
3
  export { Statement } from './statement.js';
4
4
  export { DatabaseError } from './error.js';
5
+ export { ENCRYPTION_KEY_HEADER } from './protocol.js';
@@ -51,9 +51,21 @@ export interface SequenceRequest {
51
51
  export interface CloseRequest {
52
52
  type: 'close';
53
53
  }
54
+ export interface DescribeRequest {
55
+ type: 'describe';
56
+ sql: string;
57
+ }
58
+ export interface DescribeResult {
59
+ params: Array<{
60
+ name?: string;
61
+ }>;
62
+ cols: Column[];
63
+ is_explain: boolean;
64
+ is_readonly: boolean;
65
+ }
54
66
  export interface PipelineRequest {
55
67
  baton: string | null;
56
- requests: (ExecuteRequest | BatchRequest | SequenceRequest | CloseRequest)[];
68
+ requests: (ExecuteRequest | BatchRequest | SequenceRequest | CloseRequest | DescribeRequest)[];
57
69
  }
58
70
  export interface PipelineResponse {
59
71
  baton: string | null;
@@ -61,8 +73,8 @@ export interface PipelineResponse {
61
73
  results: Array<{
62
74
  type: 'ok' | 'error';
63
75
  response?: {
64
- type: 'execute' | 'batch' | 'sequence' | 'close';
65
- result?: ExecuteResult;
76
+ type: 'execute' | 'batch' | 'sequence' | 'close' | 'describe';
77
+ result?: ExecuteResult | DescribeResult;
66
78
  };
67
79
  error?: {
68
80
  message: string;
@@ -71,7 +83,7 @@ export interface PipelineResponse {
71
83
  }>;
72
84
  }
73
85
  export declare function encodeValue(value: any): Value;
74
- export declare function decodeValue(value: Value): any;
86
+ export declare function decodeValue(value: Value, safeIntegers?: boolean): any;
75
87
  export interface CursorRequest {
76
88
  baton: string | null;
77
89
  batch: {
@@ -94,8 +106,10 @@ export interface CursorEntry {
94
106
  code: string;
95
107
  };
96
108
  }
97
- export declare function executeCursor(url: string, authToken: string, request: CursorRequest): Promise<{
109
+ /** HTTP header key for the encryption key */
110
+ export declare const ENCRYPTION_KEY_HEADER = "x-turso-encryption-key";
111
+ export declare function executeCursor(url: string, authToken: string | undefined, request: CursorRequest, remoteEncryptionKey?: string): Promise<{
98
112
  response: CursorResponse;
99
113
  entries: AsyncGenerator<CursorEntry>;
100
114
  }>;
101
- export declare function executePipeline(url: string, authToken: string, request: PipelineRequest): Promise<PipelineResponse>;
115
+ export declare function executePipeline(url: string, authToken: string | undefined, request: PipelineRequest, remoteEncryptionKey?: string): Promise<PipelineResponse>;
package/dist/protocol.js CHANGED
@@ -4,11 +4,17 @@ export function encodeValue(value) {
4
4
  return { type: 'null' };
5
5
  }
6
6
  if (typeof value === 'number') {
7
- if (Number.isInteger(value)) {
8
- return { type: 'integer', value: value.toString() };
7
+ if (!Number.isFinite(value)) {
8
+ throw new Error("Only finite numbers (not Infinity or NaN) can be passed as arguments");
9
9
  }
10
10
  return { type: 'float', value };
11
11
  }
12
+ if (typeof value === 'bigint') {
13
+ return { type: 'integer', value: value.toString() };
14
+ }
15
+ if (typeof value === 'boolean') {
16
+ return { type: 'integer', value: value ? '1' : '0' };
17
+ }
12
18
  if (typeof value === 'string') {
13
19
  return { type: 'text', value };
14
20
  }
@@ -18,11 +24,14 @@ export function encodeValue(value) {
18
24
  }
19
25
  return { type: 'text', value: String(value) };
20
26
  }
21
- export function decodeValue(value) {
27
+ export function decodeValue(value, safeIntegers = false) {
22
28
  switch (value.type) {
23
29
  case 'null':
24
30
  return null;
25
31
  case 'integer':
32
+ if (safeIntegers) {
33
+ return BigInt(value.value);
34
+ }
26
35
  return parseInt(value.value, 10);
27
36
  case 'float':
28
37
  return value.value;
@@ -35,20 +44,28 @@ export function decodeValue(value) {
35
44
  for (let i = 0; i < binaryString.length; i++) {
36
45
  bytes[i] = binaryString.charCodeAt(i);
37
46
  }
38
- return bytes;
47
+ return Buffer.from(bytes);
39
48
  }
40
49
  return null;
41
50
  default:
42
51
  return null;
43
52
  }
44
53
  }
45
- export async function executeCursor(url, authToken, request) {
54
+ /** HTTP header key for the encryption key */
55
+ export const ENCRYPTION_KEY_HEADER = 'x-turso-encryption-key';
56
+ export async function executeCursor(url, authToken, request, remoteEncryptionKey) {
57
+ const headers = {
58
+ 'Content-Type': 'application/json',
59
+ };
60
+ if (authToken) {
61
+ headers['Authorization'] = `Bearer ${authToken}`;
62
+ }
63
+ if (remoteEncryptionKey) {
64
+ headers[ENCRYPTION_KEY_HEADER] = remoteEncryptionKey;
65
+ }
46
66
  const response = await fetch(`${url}/v3/cursor`, {
47
67
  method: 'POST',
48
- headers: {
49
- 'Content-Type': 'application/json',
50
- 'Authorization': `Bearer ${authToken}`,
51
- },
68
+ headers,
52
69
  body: JSON.stringify(request),
53
70
  });
54
71
  if (!response.ok) {
@@ -127,13 +144,19 @@ export async function executeCursor(url, authToken, request) {
127
144
  }
128
145
  return { response: cursorResponse, entries: parseEntries() };
129
146
  }
130
- export async function executePipeline(url, authToken, request) {
147
+ export async function executePipeline(url, authToken, request, remoteEncryptionKey) {
148
+ const headers = {
149
+ 'Content-Type': 'application/json',
150
+ };
151
+ if (authToken) {
152
+ headers['Authorization'] = `Bearer ${authToken}`;
153
+ }
154
+ if (remoteEncryptionKey) {
155
+ headers[ENCRYPTION_KEY_HEADER] = remoteEncryptionKey;
156
+ }
131
157
  const response = await fetch(`${url}/v3/pipeline`, {
132
158
  method: 'POST',
133
- headers: {
134
- 'Content-Type': 'application/json',
135
- 'Authorization': `Bearer ${authToken}`,
136
- },
159
+ headers,
137
160
  body: JSON.stringify(request),
138
161
  });
139
162
  if (!response.ok) {
package/dist/session.d.ts CHANGED
@@ -1,12 +1,17 @@
1
- import { type CursorResponse, type CursorEntry } from './protocol.js';
1
+ import { type CursorResponse, type CursorEntry, type DescribeResult } from './protocol.js';
2
2
  /**
3
3
  * Configuration options for a session.
4
4
  */
5
5
  export interface SessionConfig {
6
6
  /** Database URL */
7
7
  url: string;
8
- /** Authentication token */
9
- authToken: string;
8
+ /** Authentication token (optional for local development with turso dev) */
9
+ authToken?: string;
10
+ /**
11
+ * Encryption key for the remote database (base64 encoded)
12
+ * to enable access to encrypted Turso Cloud databases.
13
+ */
14
+ remoteEncryptionKey?: string;
10
15
  }
11
16
  /**
12
17
  * A database session that manages the connection state and baton.
@@ -19,14 +24,22 @@ export declare class Session {
19
24
  private baton;
20
25
  private baseUrl;
21
26
  constructor(config: SessionConfig);
27
+ /**
28
+ * Describe a SQL statement to get its column metadata.
29
+ *
30
+ * @param sql - The SQL statement to describe
31
+ * @returns Promise resolving to the statement description
32
+ */
33
+ describe(sql: string): Promise<DescribeResult>;
22
34
  /**
23
35
  * Execute a SQL statement and return all results.
24
36
  *
25
37
  * @param sql - The SQL statement to execute
26
38
  * @param args - Optional array of parameter values or object with named parameters
39
+ * @param safeIntegers - Whether to return integers as BigInt
27
40
  * @returns Promise resolving to the complete result set
28
41
  */
29
- execute(sql: string, args?: any[] | Record<string, any>): Promise<any>;
42
+ execute(sql: string, args?: any[] | Record<string, any>, safeIntegers?: boolean): Promise<any>;
30
43
  /**
31
44
  * Execute a SQL statement and return the raw response and entries.
32
45
  *
@@ -44,7 +57,7 @@ export declare class Session {
44
57
  * @param entries - Async generator of cursor entries
45
58
  * @returns Promise resolving to the processed result
46
59
  */
47
- processCursorEntries(entries: AsyncGenerator<CursorEntry>): Promise<any>;
60
+ processCursorEntries(entries: AsyncGenerator<CursorEntry>, safeIntegers?: boolean): Promise<any>;
48
61
  /**
49
62
  * Create a row object with both array and named property access.
50
63
  *
package/dist/session.js CHANGED
@@ -18,16 +18,48 @@ export class Session {
18
18
  this.config = config;
19
19
  this.baseUrl = normalizeUrl(config.url);
20
20
  }
21
+ /**
22
+ * Describe a SQL statement to get its column metadata.
23
+ *
24
+ * @param sql - The SQL statement to describe
25
+ * @returns Promise resolving to the statement description
26
+ */
27
+ async describe(sql) {
28
+ const request = {
29
+ baton: this.baton,
30
+ requests: [{
31
+ type: "describe",
32
+ sql: sql
33
+ }]
34
+ };
35
+ const response = await executePipeline(this.baseUrl, this.config.authToken, request, this.config.remoteEncryptionKey);
36
+ this.baton = response.baton;
37
+ if (response.base_url) {
38
+ this.baseUrl = response.base_url;
39
+ }
40
+ // Check for errors in the response
41
+ if (response.results && response.results[0]) {
42
+ const result = response.results[0];
43
+ if (result.type === "error") {
44
+ throw new DatabaseError(result.error?.message || 'Describe execution failed');
45
+ }
46
+ if (result.response?.type === "describe" && result.response.result) {
47
+ return result.response.result;
48
+ }
49
+ }
50
+ throw new DatabaseError('Unexpected describe response');
51
+ }
21
52
  /**
22
53
  * Execute a SQL statement and return all results.
23
54
  *
24
55
  * @param sql - The SQL statement to execute
25
56
  * @param args - Optional array of parameter values or object with named parameters
57
+ * @param safeIntegers - Whether to return integers as BigInt
26
58
  * @returns Promise resolving to the complete result set
27
59
  */
28
- async execute(sql, args = []) {
60
+ async execute(sql, args = [], safeIntegers = false) {
29
61
  const { response, entries } = await this.executeRaw(sql, args);
30
- const result = await this.processCursorEntries(entries);
62
+ const result = await this.processCursorEntries(entries, safeIntegers);
31
63
  return result;
32
64
  }
33
65
  /**
@@ -86,7 +118,7 @@ export class Session {
86
118
  }]
87
119
  }
88
120
  };
89
- const { response, entries } = await executeCursor(this.baseUrl, this.config.authToken, request);
121
+ const { response, entries } = await executeCursor(this.baseUrl, this.config.authToken, request, this.config.remoteEncryptionKey);
90
122
  this.baton = response.baton;
91
123
  if (response.base_url) {
92
124
  this.baseUrl = response.base_url;
@@ -99,7 +131,7 @@ export class Session {
99
131
  * @param entries - Async generator of cursor entries
100
132
  * @returns Promise resolving to the processed result
101
133
  */
102
- async processCursorEntries(entries) {
134
+ async processCursorEntries(entries, safeIntegers = false) {
103
135
  let columns = [];
104
136
  let columnTypes = [];
105
137
  let rows = [];
@@ -115,7 +147,7 @@ export class Session {
115
147
  break;
116
148
  case 'row':
117
149
  if (entry.row) {
118
- const decodedRow = entry.row.map(decodeValue);
150
+ const decodedRow = entry.row.map(value => decodeValue(value, safeIntegers));
119
151
  const rowObject = this.createRowObject(decodedRow, columns);
120
152
  rows.push(rowObject);
121
153
  }
@@ -184,7 +216,7 @@ export class Session {
184
216
  }))
185
217
  }
186
218
  };
187
- const { response, entries } = await executeCursor(this.baseUrl, this.config.authToken, request);
219
+ const { response, entries } = await executeCursor(this.baseUrl, this.config.authToken, request, this.config.remoteEncryptionKey);
188
220
  this.baton = response.baton;
189
221
  if (response.base_url) {
190
222
  this.baseUrl = response.base_url;
@@ -225,7 +257,7 @@ export class Session {
225
257
  sql: sql
226
258
  }]
227
259
  };
228
- const response = await executePipeline(this.baseUrl, this.config.authToken, request);
260
+ const response = await executePipeline(this.baseUrl, this.config.authToken, request, this.config.remoteEncryptionKey);
229
261
  this.baton = response.baton;
230
262
  if (response.base_url) {
231
263
  this.baseUrl = response.base_url;
@@ -254,7 +286,7 @@ export class Session {
254
286
  type: "close"
255
287
  }]
256
288
  };
257
- await executePipeline(this.baseUrl, this.config.authToken, request);
289
+ await executePipeline(this.baseUrl, this.config.authToken, request, this.config.remoteEncryptionKey);
258
290
  }
259
291
  catch (error) {
260
292
  // Ignore errors during close, as the connection might already be closed
@@ -1,3 +1,4 @@
1
+ import { type Column } from './protocol.js';
1
2
  import { type SessionConfig } from './session.js';
2
3
  /**
3
4
  * A prepared SQL statement that can be executed in multiple ways.
@@ -12,7 +13,26 @@ export declare class Statement {
12
13
  private session;
13
14
  private sql;
14
15
  private presentationMode;
15
- constructor(sessionConfig: SessionConfig, sql: string);
16
+ private safeIntegerMode;
17
+ private columnMetadata;
18
+ constructor(sessionConfig: SessionConfig, sql: string, columns?: Column[]);
19
+ /**
20
+ * Whether the prepared statement returns data.
21
+ *
22
+ * This is `true` for SELECT queries and statements with RETURNING clause,
23
+ * and `false` for INSERT, UPDATE, DELETE statements without RETURNING.
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const stmt = await conn.prepare(sql);
28
+ * if (stmt.reader) {
29
+ * return stmt.all(args); // SELECT-like query
30
+ * } else {
31
+ * return stmt.run(args); // INSERT/UPDATE/DELETE
32
+ * }
33
+ * ```
34
+ */
35
+ get reader(): boolean;
16
36
  /**
17
37
  * Enable raw mode to return arrays instead of objects.
18
38
  *
@@ -27,6 +47,40 @@ export declare class Statement {
27
47
  * ```
28
48
  */
29
49
  raw(raw?: boolean): Statement;
50
+ /**
51
+ * Enable pluck mode to return only the first column value from each row.
52
+ *
53
+ * @param pluck Enable or disable pluck mode. If you don't pass the parameter, pluck mode is enabled.
54
+ * @returns This statement instance for chaining
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * const stmt = client.prepare("SELECT id FROM users");
59
+ * const ids = await stmt.pluck().all();
60
+ * console.log(ids); // [1, 2, 3, ...]
61
+ * ```
62
+ */
63
+ pluck(pluck?: boolean): Statement;
64
+ /**
65
+ * Sets safe integers mode for this statement.
66
+ *
67
+ * @param toggle Whether to use safe integers. If you don't pass the parameter, safe integers mode is enabled.
68
+ * @returns This statement instance for chaining
69
+ */
70
+ safeIntegers(toggle?: boolean): Statement;
71
+ /**
72
+ * Get column information for this statement.
73
+ *
74
+ * @returns Array of column metadata objects matching the native bindings format
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * const stmt = await client.prepare("SELECT id, name, email FROM users");
79
+ * const columns = stmt.columns();
80
+ * console.log(columns); // [{ name: 'id', type: 'INTEGER', column: null, database: null, table: null }, ...]
81
+ * ```
82
+ */
83
+ columns(): any[];
30
84
  /**
31
85
  * Executes the prepared statement.
32
86
  *
package/dist/statement.js CHANGED
@@ -11,10 +11,31 @@ import { DatabaseError } from './error.js';
11
11
  * - `iterate(args?)`: Returns an async iterator for streaming results
12
12
  */
13
13
  export class Statement {
14
- constructor(sessionConfig, sql) {
14
+ constructor(sessionConfig, sql, columns) {
15
15
  this.presentationMode = 'expanded';
16
+ this.safeIntegerMode = false;
16
17
  this.session = new Session(sessionConfig);
17
18
  this.sql = sql;
19
+ this.columnMetadata = columns || [];
20
+ }
21
+ /**
22
+ * Whether the prepared statement returns data.
23
+ *
24
+ * This is `true` for SELECT queries and statements with RETURNING clause,
25
+ * and `false` for INSERT, UPDATE, DELETE statements without RETURNING.
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * const stmt = await conn.prepare(sql);
30
+ * if (stmt.reader) {
31
+ * return stmt.all(args); // SELECT-like query
32
+ * } else {
33
+ * return stmt.run(args); // INSERT/UPDATE/DELETE
34
+ * }
35
+ * ```
36
+ */
37
+ get reader() {
38
+ return this.columnMetadata.length > 0;
18
39
  }
19
40
  /**
20
41
  * Enable raw mode to return arrays instead of objects.
@@ -33,6 +54,51 @@ export class Statement {
33
54
  this.presentationMode = raw === false ? 'expanded' : 'raw';
34
55
  return this;
35
56
  }
57
+ /**
58
+ * Enable pluck mode to return only the first column value from each row.
59
+ *
60
+ * @param pluck Enable or disable pluck mode. If you don't pass the parameter, pluck mode is enabled.
61
+ * @returns This statement instance for chaining
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * const stmt = client.prepare("SELECT id FROM users");
66
+ * const ids = await stmt.pluck().all();
67
+ * console.log(ids); // [1, 2, 3, ...]
68
+ * ```
69
+ */
70
+ pluck(pluck) {
71
+ this.presentationMode = pluck === false ? 'expanded' : 'pluck';
72
+ return this;
73
+ }
74
+ /**
75
+ * Sets safe integers mode for this statement.
76
+ *
77
+ * @param toggle Whether to use safe integers. If you don't pass the parameter, safe integers mode is enabled.
78
+ * @returns This statement instance for chaining
79
+ */
80
+ safeIntegers(toggle) {
81
+ this.safeIntegerMode = toggle === false ? false : true;
82
+ return this;
83
+ }
84
+ /**
85
+ * Get column information for this statement.
86
+ *
87
+ * @returns Array of column metadata objects matching the native bindings format
88
+ *
89
+ * @example
90
+ * ```typescript
91
+ * const stmt = await client.prepare("SELECT id, name, email FROM users");
92
+ * const columns = stmt.columns();
93
+ * console.log(columns); // [{ name: 'id', type: 'INTEGER', column: null, database: null, table: null }, ...]
94
+ * ```
95
+ */
96
+ columns() {
97
+ return this.columnMetadata.map(col => ({
98
+ name: col.name,
99
+ type: col.decltype
100
+ }));
101
+ }
36
102
  /**
37
103
  * Executes the prepared statement.
38
104
  *
@@ -48,7 +114,7 @@ export class Statement {
48
114
  */
49
115
  async run(args) {
50
116
  const normalizedArgs = this.normalizeArgs(args);
51
- const result = await this.session.execute(this.sql, normalizedArgs);
117
+ const result = await this.session.execute(this.sql, normalizedArgs, this.safeIntegerMode);
52
118
  return { changes: result.rowsAffected, lastInsertRowid: result.lastInsertRowid };
53
119
  }
54
120
  /**
@@ -68,17 +134,26 @@ export class Statement {
68
134
  */
69
135
  async get(args) {
70
136
  const normalizedArgs = this.normalizeArgs(args);
71
- const result = await this.session.execute(this.sql, normalizedArgs);
137
+ const result = await this.session.execute(this.sql, normalizedArgs, this.safeIntegerMode);
72
138
  const row = result.rows[0];
73
139
  if (!row) {
74
140
  return undefined;
75
141
  }
142
+ if (this.presentationMode === 'pluck') {
143
+ // In pluck mode, return only the first column value
144
+ return row[0];
145
+ }
76
146
  if (this.presentationMode === 'raw') {
77
147
  // In raw mode, return the row as a plain array (it already is one)
78
148
  // The row object is already an array with column properties added
79
149
  return [...row];
80
150
  }
81
- return row;
151
+ // In expanded mode, convert to plain object with named properties
152
+ const obj = {};
153
+ result.columns.forEach((col, i) => {
154
+ obj[col] = row[i];
155
+ });
156
+ return obj;
82
157
  }
83
158
  /**
84
159
  * Execute the statement and return all rows.
@@ -95,10 +170,15 @@ export class Statement {
95
170
  */
96
171
  async all(args) {
97
172
  const normalizedArgs = this.normalizeArgs(args);
98
- const result = await this.session.execute(this.sql, normalizedArgs);
173
+ const result = await this.session.execute(this.sql, normalizedArgs, this.safeIntegerMode);
174
+ if (this.presentationMode === 'pluck') {
175
+ // In pluck mode, return only the first column value from each row
176
+ return result.rows.map((row) => row[0]);
177
+ }
99
178
  if (this.presentationMode === 'raw') {
100
179
  return result.rows.map((row) => [...row]);
101
180
  }
181
+ // In expanded mode, convert rows to plain objects with named properties
102
182
  return result.rows.map((row) => {
103
183
  const obj = {};
104
184
  result.columns.forEach((col, i) => {
@@ -138,14 +218,22 @@ export class Statement {
138
218
  break;
139
219
  case 'row':
140
220
  if (entry.row) {
141
- const decodedRow = entry.row.map(decodeValue);
142
- if (this.presentationMode === 'raw') {
221
+ const decodedRow = entry.row.map(value => decodeValue(value, this.safeIntegerMode));
222
+ if (this.presentationMode === 'pluck') {
223
+ // In pluck mode, yield only the first column value
224
+ yield decodedRow[0];
225
+ }
226
+ else if (this.presentationMode === 'raw') {
143
227
  // In raw mode, yield arrays of values
144
228
  yield decodedRow;
145
229
  }
146
230
  else {
147
- const rowObject = this.session.createRowObject(decodedRow, columns);
148
- yield rowObject;
231
+ // In expanded mode, yield plain objects with named properties (consistent with all())
232
+ const obj = {};
233
+ columns.forEach((col, i) => {
234
+ obj[col] = decodedRow[i];
235
+ });
236
+ yield obj;
149
237
  }
150
238
  }
151
239
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tursodatabase/serverless",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -30,6 +30,6 @@
30
30
  "devDependencies": {
31
31
  "@types/node": "^24.0.13",
32
32
  "ava": "^6.4.1",
33
- "typescript": "^5.8.3"
33
+ "typescript": "^5.9.2"
34
34
  }
35
35
  }
@@ -1,131 +0,0 @@
1
- import { type SessionConfig } from "./session.js";
2
- /**
3
- * Transaction mode for controlling transaction behavior.
4
- */
5
- export type TransactionMode = "write" | "read" | "deferred";
6
- /**
7
- * Transactions use a dedicated session to maintain state across multiple operations.
8
- * All operations within a transaction are executed atomically - they either all
9
- * succeed or all fail.
10
- */
11
- export declare class Transaction {
12
- private session;
13
- private _closed;
14
- private _committed;
15
- private _rolledBack;
16
- private constructor();
17
- /**
18
- * Create a new transaction instance.
19
- *
20
- * @param sessionConfig - Session configuration
21
- * @param mode - Transaction mode
22
- * @returns Promise resolving to a new Transaction instance
23
- */
24
- static create(sessionConfig: SessionConfig, mode?: TransactionMode): Promise<Transaction>;
25
- private initializeTransaction;
26
- /**
27
- * Execute a SQL statement within the transaction.
28
- *
29
- * @param sql - The SQL statement to execute
30
- * @param args - Optional array of parameter values
31
- * @returns Promise resolving to the complete result set
32
- *
33
- * @example
34
- * ```typescript
35
- * const tx = await client.transaction();
36
- * const result = await tx.execute("INSERT INTO users (name) VALUES (?)", ["Alice"]);
37
- * await tx.commit();
38
- * ```
39
- */
40
- execute(sql: string, args?: any[]): Promise<any>;
41
- /**
42
- * Execute multiple SQL statements as a batch within the transaction.
43
- *
44
- * @param statements - Array of SQL statements to execute
45
- * @returns Promise resolving to batch execution results
46
- *
47
- * @example
48
- * ```typescript
49
- * const tx = await client.transaction();
50
- * await tx.batch([
51
- * "INSERT INTO users (name) VALUES ('Alice')",
52
- * "INSERT INTO users (name) VALUES ('Bob')"
53
- * ]);
54
- * await tx.commit();
55
- * ```
56
- */
57
- batch(statements: string[]): Promise<any>;
58
- /**
59
- * Execute a SQL statement and return the raw response and entries.
60
- *
61
- * @param sql - The SQL statement to execute
62
- * @param args - Optional array of parameter values
63
- * @returns Promise resolving to the raw response and cursor entries
64
- */
65
- executeRaw(sql: string, args?: any[]): Promise<{
66
- response: any;
67
- entries: AsyncGenerator<any>;
68
- }>;
69
- /**
70
- * Commit the transaction, making all changes permanent.
71
- *
72
- * @returns Promise that resolves when the transaction is committed
73
- *
74
- * @example
75
- * ```typescript
76
- * const tx = await client.transaction();
77
- * await tx.execute("INSERT INTO users (name) VALUES (?)", ["Alice"]);
78
- * await tx.commit(); // Changes are now permanent
79
- * ```
80
- */
81
- commit(): Promise<void>;
82
- /**
83
- * Rollback the transaction, undoing all changes.
84
- *
85
- * @returns Promise that resolves when the transaction is rolled back
86
- *
87
- * @example
88
- * ```typescript
89
- * const tx = await client.transaction();
90
- * try {
91
- * await tx.execute("INSERT INTO users (name) VALUES (?)", ["Alice"]);
92
- * await tx.execute("INSERT INTO invalid_table VALUES (1)"); // This will fail
93
- * await tx.commit();
94
- * } catch (error) {
95
- * await tx.rollback(); // Undo the first INSERT
96
- * }
97
- * ```
98
- */
99
- rollback(): Promise<void>;
100
- /**
101
- * Close the transaction without committing or rolling back.
102
- *
103
- * This will automatically rollback any uncommitted changes.
104
- * It's safe to call this method multiple times.
105
- */
106
- close(): void;
107
- /**
108
- * Check if the transaction is closed.
109
- *
110
- * @returns True if the transaction has been committed, rolled back, or closed
111
- */
112
- get closed(): boolean;
113
- /**
114
- * Check if the transaction has been committed.
115
- *
116
- * @returns True if the transaction has been successfully committed
117
- */
118
- get committed(): boolean;
119
- /**
120
- * Check if the transaction has been rolled back.
121
- *
122
- * @returns True if the transaction has been rolled back
123
- */
124
- get rolledBack(): boolean;
125
- /**
126
- * Check transaction state and throw if it's not valid for operations.
127
- *
128
- * @throws Error if the transaction is closed
129
- */
130
- private checkState;
131
- }
@@ -1,207 +0,0 @@
1
- import { Session } from "./session.js";
2
- import { DatabaseError } from "./error.js";
3
- /**
4
- * Transactions use a dedicated session to maintain state across multiple operations.
5
- * All operations within a transaction are executed atomically - they either all
6
- * succeed or all fail.
7
- */
8
- export class Transaction {
9
- constructor(sessionConfig, mode = "deferred") {
10
- this._closed = false;
11
- this._committed = false;
12
- this._rolledBack = false;
13
- this.session = new Session(sessionConfig);
14
- }
15
- /**
16
- * Create a new transaction instance.
17
- *
18
- * @param sessionConfig - Session configuration
19
- * @param mode - Transaction mode
20
- * @returns Promise resolving to a new Transaction instance
21
- */
22
- static async create(sessionConfig, mode = "deferred") {
23
- const transaction = new Transaction(sessionConfig, mode);
24
- await transaction.initializeTransaction(mode);
25
- return transaction;
26
- }
27
- async initializeTransaction(mode) {
28
- let beginStatement;
29
- switch (mode) {
30
- case "write":
31
- beginStatement = "BEGIN IMMEDIATE";
32
- break;
33
- case "read":
34
- beginStatement = "BEGIN";
35
- break;
36
- case "deferred":
37
- default:
38
- beginStatement = "BEGIN DEFERRED";
39
- break;
40
- }
41
- await this.session.execute(beginStatement);
42
- }
43
- /**
44
- * Execute a SQL statement within the transaction.
45
- *
46
- * @param sql - The SQL statement to execute
47
- * @param args - Optional array of parameter values
48
- * @returns Promise resolving to the complete result set
49
- *
50
- * @example
51
- * ```typescript
52
- * const tx = await client.transaction();
53
- * const result = await tx.execute("INSERT INTO users (name) VALUES (?)", ["Alice"]);
54
- * await tx.commit();
55
- * ```
56
- */
57
- async execute(sql, args = []) {
58
- this.checkState();
59
- return this.session.execute(sql, args);
60
- }
61
- /**
62
- * Execute multiple SQL statements as a batch within the transaction.
63
- *
64
- * @param statements - Array of SQL statements to execute
65
- * @returns Promise resolving to batch execution results
66
- *
67
- * @example
68
- * ```typescript
69
- * const tx = await client.transaction();
70
- * await tx.batch([
71
- * "INSERT INTO users (name) VALUES ('Alice')",
72
- * "INSERT INTO users (name) VALUES ('Bob')"
73
- * ]);
74
- * await tx.commit();
75
- * ```
76
- */
77
- async batch(statements) {
78
- this.checkState();
79
- return this.session.batch(statements);
80
- }
81
- /**
82
- * Execute a SQL statement and return the raw response and entries.
83
- *
84
- * @param sql - The SQL statement to execute
85
- * @param args - Optional array of parameter values
86
- * @returns Promise resolving to the raw response and cursor entries
87
- */
88
- async executeRaw(sql, args = []) {
89
- this.checkState();
90
- return this.session.executeRaw(sql, args);
91
- }
92
- /**
93
- * Commit the transaction, making all changes permanent.
94
- *
95
- * @returns Promise that resolves when the transaction is committed
96
- *
97
- * @example
98
- * ```typescript
99
- * const tx = await client.transaction();
100
- * await tx.execute("INSERT INTO users (name) VALUES (?)", ["Alice"]);
101
- * await tx.commit(); // Changes are now permanent
102
- * ```
103
- */
104
- async commit() {
105
- this.checkState();
106
- try {
107
- await this.session.execute("COMMIT");
108
- this._committed = true;
109
- this._closed = true;
110
- }
111
- catch (error) {
112
- // If commit fails, the transaction is still open
113
- if (error instanceof Error) {
114
- throw new DatabaseError(error.message);
115
- }
116
- throw new DatabaseError('Transaction commit failed');
117
- }
118
- }
119
- /**
120
- * Rollback the transaction, undoing all changes.
121
- *
122
- * @returns Promise that resolves when the transaction is rolled back
123
- *
124
- * @example
125
- * ```typescript
126
- * const tx = await client.transaction();
127
- * try {
128
- * await tx.execute("INSERT INTO users (name) VALUES (?)", ["Alice"]);
129
- * await tx.execute("INSERT INTO invalid_table VALUES (1)"); // This will fail
130
- * await tx.commit();
131
- * } catch (error) {
132
- * await tx.rollback(); // Undo the first INSERT
133
- * }
134
- * ```
135
- */
136
- async rollback() {
137
- if (this._closed) {
138
- return; // Already closed, nothing to rollback
139
- }
140
- try {
141
- await this.session.execute("ROLLBACK");
142
- }
143
- catch (error) {
144
- // Rollback errors are generally not critical - the transaction is abandoned anyway
145
- }
146
- finally {
147
- this._rolledBack = true;
148
- this._closed = true;
149
- }
150
- }
151
- /**
152
- * Close the transaction without committing or rolling back.
153
- *
154
- * This will automatically rollback any uncommitted changes.
155
- * It's safe to call this method multiple times.
156
- */
157
- close() {
158
- if (!this._closed) {
159
- // Async rollback - don't wait for it to complete
160
- this.rollback().catch(() => {
161
- // Ignore rollback errors on close
162
- });
163
- }
164
- }
165
- /**
166
- * Check if the transaction is closed.
167
- *
168
- * @returns True if the transaction has been committed, rolled back, or closed
169
- */
170
- get closed() {
171
- return this._closed;
172
- }
173
- /**
174
- * Check if the transaction has been committed.
175
- *
176
- * @returns True if the transaction has been successfully committed
177
- */
178
- get committed() {
179
- return this._committed;
180
- }
181
- /**
182
- * Check if the transaction has been rolled back.
183
- *
184
- * @returns True if the transaction has been rolled back
185
- */
186
- get rolledBack() {
187
- return this._rolledBack;
188
- }
189
- /**
190
- * Check transaction state and throw if it's not valid for operations.
191
- *
192
- * @throws Error if the transaction is closed
193
- */
194
- checkState() {
195
- if (this._closed) {
196
- if (this._committed) {
197
- throw new DatabaseError("Transaction has already been committed");
198
- }
199
- else if (this._rolledBack) {
200
- throw new DatabaseError("Transaction has already been rolled back");
201
- }
202
- else {
203
- throw new DatabaseError("Transaction has been closed");
204
- }
205
- }
206
- }
207
- }