UNPKG

@libsql/client

Version:
501 lines (500 loc) 17.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Sqlite3Transaction = exports.Sqlite3Client = exports._createClient = exports.createClient = void 0; const libsql_1 = __importDefault(require("libsql")); const node_buffer_1 = require("node:buffer"); const api_1 = require("@libsql/core/api"); const config_1 = require("@libsql/core/config"); const util_1 = require("@libsql/core/util"); __exportStar(require("@libsql/core/api"), exports); function createClient(config) { return _createClient((0, config_1.expandConfig)(config, true)); } exports.createClient = createClient; /** @private */ function _createClient(config) { if (config.scheme !== "file") { throw new api_1.LibsqlError(`URL scheme ${JSON.stringify(config.scheme + ":")} is not supported by the local sqlite3 client. ` + `For more information, please read ${util_1.supportedUrlLink}`, "URL_SCHEME_NOT_SUPPORTED"); } const authority = config.authority; if (authority !== undefined) { const host = authority.host.toLowerCase(); if (host !== "" && host !== "localhost") { throw new api_1.LibsqlError(`Invalid host in file URL: ${JSON.stringify(authority.host)}. ` + 'A "file:" URL with an absolute path should start with one slash ("file:/absolute/path.db") ' + 'or with three slashes ("file:///absolute/path.db"). ' + `For more information, please read ${util_1.supportedUrlLink}`, "URL_INVALID"); } if (authority.port !== undefined) { throw new api_1.LibsqlError("File URL cannot have a port", "URL_INVALID"); } if (authority.userinfo !== undefined) { throw new api_1.LibsqlError("File URL cannot have username and password", "URL_INVALID"); } } let isInMemory = (0, config_1.isInMemoryConfig)(config); if (isInMemory && config.syncUrl) { throw new api_1.LibsqlError(`Embedded replica must use file for local db but URI with in-memory mode were provided instead: ${config.path}`, "URL_INVALID"); } let path = config.path; if (isInMemory) { // note: we should prepend file scheme in order for SQLite3 to recognize :memory: connection query parameters path = `${config.scheme}:${config.path}`; } const options = { authToken: config.authToken, encryptionKey: config.encryptionKey, remoteEncryptionKey: config.remoteEncryptionKey, syncUrl: config.syncUrl, syncPeriod: config.syncInterval, readYourWrites: config.readYourWrites, offline: config.offline, }; const db = new libsql_1.default(path, options); executeStmt(db, "SELECT 1 AS checkThatTheDatabaseCanBeOpened", config.intMode); return new Sqlite3Client(path, options, db, config.intMode); } exports._createClient = _createClient; class Sqlite3Client { #path; #options; #db; #intMode; closed; protocol; /** @private */ constructor(path, options, db, intMode) { this.#path = path; this.#options = options; this.#db = db; this.#intMode = intMode; this.closed = false; this.protocol = "file"; } async execute(stmtOrSql, args) { let stmt; if (typeof stmtOrSql === "string") { stmt = { sql: stmtOrSql, args: args || [], }; } else { stmt = stmtOrSql; } this.#checkNotClosed(); return executeStmt(this.#getDb(), stmt, this.#intMode); } async batch(stmts, mode = "deferred") { this.#checkNotClosed(); const db = this.#getDb(); try { executeStmt(db, (0, util_1.transactionModeToBegin)(mode), this.#intMode); const resultSets = []; for (let i = 0; i < stmts.length; i++) { try { if (!db.inTransaction) { throw new api_1.LibsqlBatchError("The transaction has been rolled back", i, "TRANSACTION_CLOSED"); } const stmt = stmts[i]; const normalizedStmt = Array.isArray(stmt) ? { sql: stmt[0], args: stmt[1] || [] } : stmt; resultSets.push(executeStmt(db, normalizedStmt, this.#intMode)); } catch (e) { if (e instanceof api_1.LibsqlBatchError) { throw e; } if (e instanceof api_1.LibsqlError) { throw new api_1.LibsqlBatchError(e.message, i, e.code, e.extendedCode, e.rawCode, e.cause instanceof Error ? e.cause : undefined); } throw e; } } executeStmt(db, "COMMIT", this.#intMode); return resultSets; } finally { if (db.inTransaction) { executeStmt(db, "ROLLBACK", this.#intMode); } } } async migrate(stmts) { this.#checkNotClosed(); const db = this.#getDb(); try { executeStmt(db, "PRAGMA foreign_keys=off", this.#intMode); executeStmt(db, (0, util_1.transactionModeToBegin)("deferred"), this.#intMode); const resultSets = []; for (let i = 0; i < stmts.length; i++) { try { if (!db.inTransaction) { throw new api_1.LibsqlBatchError("The transaction has been rolled back", i, "TRANSACTION_CLOSED"); } resultSets.push(executeStmt(db, stmts[i], this.#intMode)); } catch (e) { if (e instanceof api_1.LibsqlBatchError) { throw e; } if (e instanceof api_1.LibsqlError) { throw new api_1.LibsqlBatchError(e.message, i, e.code, e.extendedCode, e.rawCode, e.cause instanceof Error ? e.cause : undefined); } throw e; } } executeStmt(db, "COMMIT", this.#intMode); return resultSets; } finally { if (db.inTransaction) { executeStmt(db, "ROLLBACK", this.#intMode); } executeStmt(db, "PRAGMA foreign_keys=on", this.#intMode); } } async transaction(mode = "write") { const db = this.#getDb(); executeStmt(db, (0, util_1.transactionModeToBegin)(mode), this.#intMode); this.#db = null; // A new connection will be lazily created on next use return new Sqlite3Transaction(db, this.#intMode); } async executeMultiple(sql) { this.#checkNotClosed(); const db = this.#getDb(); try { return executeMultiple(db, sql); } finally { if (db.inTransaction) { executeStmt(db, "ROLLBACK", this.#intMode); } } } async sync() { this.#checkNotClosed(); const rep = await this.#getDb().sync(); return { frames_synced: rep.frames_synced, frame_no: rep.frame_no, }; } async reconnect() { try { if (!this.closed && this.#db !== null) { this.#db.close(); } } finally { this.#db = new libsql_1.default(this.#path, this.#options); this.closed = false; } } close() { this.closed = true; if (this.#db !== null) { this.#db.close(); this.#db = null; } } #checkNotClosed() { if (this.closed) { throw new api_1.LibsqlError("The client is closed", "CLIENT_CLOSED"); } } // Lazily creates the database connection and returns it #getDb() { if (this.#db === null) { this.#db = new libsql_1.default(this.#path, this.#options); } return this.#db; } } exports.Sqlite3Client = Sqlite3Client; class Sqlite3Transaction { #database; #intMode; /** @private */ constructor(database, intMode) { this.#database = database; this.#intMode = intMode; } async execute(stmtOrSql, args) { let stmt; if (typeof stmtOrSql === "string") { stmt = { sql: stmtOrSql, args: args || [], }; } else { stmt = stmtOrSql; } this.#checkNotClosed(); return executeStmt(this.#database, stmt, this.#intMode); } async batch(stmts) { const resultSets = []; for (let i = 0; i < stmts.length; i++) { try { this.#checkNotClosed(); const stmt = stmts[i]; const normalizedStmt = Array.isArray(stmt) ? { sql: stmt[0], args: stmt[1] || [] } : stmt; resultSets.push(executeStmt(this.#database, normalizedStmt, this.#intMode)); } catch (e) { if (e instanceof api_1.LibsqlBatchError) { throw e; } if (e instanceof api_1.LibsqlError) { throw new api_1.LibsqlBatchError(e.message, i, e.code, e.extendedCode, e.rawCode, e.cause instanceof Error ? e.cause : undefined); } throw e; } } return resultSets; } async executeMultiple(sql) { this.#checkNotClosed(); return executeMultiple(this.#database, sql); } async rollback() { if (!this.#database.open) { return; } this.#checkNotClosed(); executeStmt(this.#database, "ROLLBACK", this.#intMode); } async commit() { this.#checkNotClosed(); executeStmt(this.#database, "COMMIT", this.#intMode); } close() { if (this.#database.inTransaction) { executeStmt(this.#database, "ROLLBACK", this.#intMode); } } get closed() { return !this.#database.inTransaction; } #checkNotClosed() { if (this.closed) { throw new api_1.LibsqlError("The transaction is closed", "TRANSACTION_CLOSED"); } } } exports.Sqlite3Transaction = Sqlite3Transaction; function executeStmt(db, stmt, intMode) { let sql; let args; if (typeof stmt === "string") { sql = stmt; args = []; } else { sql = stmt.sql; if (Array.isArray(stmt.args)) { args = stmt.args.map((value) => valueToSql(value, intMode)); } else { args = {}; for (const name in stmt.args) { const argName = name[0] === "@" || name[0] === "$" || name[0] === ":" ? name.substring(1) : name; args[argName] = valueToSql(stmt.args[name], intMode); } } } try { const sqlStmt = db.prepare(sql); sqlStmt.safeIntegers(true); let returnsData = true; try { sqlStmt.raw(true); } catch { // raw() throws an exception if the statement does not return data returnsData = false; } if (returnsData) { const columns = Array.from(sqlStmt.columns().map((col) => col.name)); const columnTypes = Array.from(sqlStmt.columns().map((col) => col.type ?? "")); const rows = sqlStmt.all(args).map((sqlRow) => { return rowFromSql(sqlRow, columns, intMode); }); // TODO: can we get this info from better-sqlite3? const rowsAffected = 0; const lastInsertRowid = undefined; return new util_1.ResultSetImpl(columns, columnTypes, rows, rowsAffected, lastInsertRowid); } else { const info = sqlStmt.run(args); const rowsAffected = info.changes; const lastInsertRowid = BigInt(info.lastInsertRowid); return new util_1.ResultSetImpl([], [], [], rowsAffected, lastInsertRowid); } } catch (e) { throw mapSqliteError(e); } } function rowFromSql(sqlRow, columns, intMode) { const row = {}; // make sure that the "length" property is not enumerable Object.defineProperty(row, "length", { value: sqlRow.length }); for (let i = 0; i < sqlRow.length; ++i) { const value = valueFromSql(sqlRow[i], intMode); Object.defineProperty(row, i, { value }); const column = columns[i]; if (!Object.hasOwn(row, column)) { Object.defineProperty(row, column, { value, enumerable: true, configurable: true, writable: true, }); } } return row; } function valueFromSql(sqlValue, intMode) { if (typeof sqlValue === "bigint") { if (intMode === "number") { if (sqlValue < minSafeBigint || sqlValue > maxSafeBigint) { throw new RangeError("Received integer which cannot be safely represented as a JavaScript number"); } return Number(sqlValue); } else if (intMode === "bigint") { return sqlValue; } else if (intMode === "string") { return "" + sqlValue; } else { throw new Error("Invalid value for IntMode"); } } else if (sqlValue instanceof node_buffer_1.Buffer) { return sqlValue.buffer; } return sqlValue; } const minSafeBigint = -9007199254740991n; const maxSafeBigint = 9007199254740991n; function valueToSql(value, intMode) { if (typeof value === "number") { if (!Number.isFinite(value)) { throw new RangeError("Only finite numbers (not Infinity or NaN) can be passed as arguments"); } return value; } else if (typeof value === "bigint") { if (value < minInteger || value > maxInteger) { throw new RangeError("bigint is too large to be represented as a 64-bit integer and passed as argument"); } return value; } else if (typeof value === "boolean") { switch (intMode) { case "bigint": return value ? 1n : 0n; case "string": return value ? "1" : "0"; default: return value ? 1 : 0; } } else if (value instanceof ArrayBuffer) { return node_buffer_1.Buffer.from(value); } else if (value instanceof Date) { return value.valueOf(); } else if (value === undefined) { throw new TypeError("undefined cannot be passed as argument to the database"); } else { return value; } } const minInteger = -9223372036854775808n; const maxInteger = 9223372036854775807n; function executeMultiple(db, sql) { try { db.exec(sql); } catch (e) { throw mapSqliteError(e); } } function mapSqliteError(e) { if (e instanceof libsql_1.default.SqliteError) { const extendedCode = e.code; const code = mapToBaseCode(e.rawCode); return new api_1.LibsqlError(e.message, code, extendedCode, e.rawCode, e); } return e; } // Map SQLite raw error code to base error code string. // Extended error codes are (base | (extended << 8)), so base = rawCode & 0xFF function mapToBaseCode(rawCode) { if (rawCode === undefined) { return "SQLITE_UNKNOWN"; } const baseCode = rawCode & 0xff; return (sqliteErrorCodes[baseCode] ?? `SQLITE_UNKNOWN_${baseCode.toString()}`); } const sqliteErrorCodes = { 1: "SQLITE_ERROR", 2: "SQLITE_INTERNAL", 3: "SQLITE_PERM", 4: "SQLITE_ABORT", 5: "SQLITE_BUSY", 6: "SQLITE_LOCKED", 7: "SQLITE_NOMEM", 8: "SQLITE_READONLY", 9: "SQLITE_INTERRUPT", 10: "SQLITE_IOERR", 11: "SQLITE_CORRUPT", 12: "SQLITE_NOTFOUND", 13: "SQLITE_FULL", 14: "SQLITE_CANTOPEN", 15: "SQLITE_PROTOCOL", 16: "SQLITE_EMPTY", 17: "SQLITE_SCHEMA", 18: "SQLITE_TOOBIG", 19: "SQLITE_CONSTRAINT", 20: "SQLITE_MISMATCH", 21: "SQLITE_MISUSE", 22: "SQLITE_NOLFS", 23: "SQLITE_AUTH", 24: "SQLITE_FORMAT", 25: "SQLITE_RANGE", 26: "SQLITE_NOTADB", 27: "SQLITE_NOTICE", 28: "SQLITE_WARNING", };