@mark1russell7/docker-sqlite 0.0.0-stage → 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,52 @@
1
+ /**
2
+ * Query helper utilities for SQLite
3
+ */
4
+ import type { Database } from "sql.js";
5
+ import type { QueryResult, ExecuteResult } from "./types.js";
6
+ /**
7
+ * Execute a SELECT query and return results
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * const result = query(db, 'SELECT * FROM users WHERE age > ?', [21]);
12
+ * console.log(result.columns); // ['id', 'name', 'age']
13
+ * console.log(result.rows); // [{ id: 1, name: 'Alice', age: 25 }, ...]
14
+ * ```
15
+ */
16
+ export declare function query<T = Record<string, unknown>>(db: Database, sql: string, params?: unknown[]): QueryResult<T>;
17
+ /**
18
+ * Execute an INSERT, UPDATE, DELETE, or other statement
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * const result = execute(db, 'INSERT INTO users (name, age) VALUES (?, ?)', ['Bob', 30]);
23
+ * console.log(result.changes); // 1
24
+ * ```
25
+ */
26
+ export declare function execute(db: Database, sql: string, params?: unknown[]): ExecuteResult;
27
+ /**
28
+ * True when `sql` holds more than one statement. Nothing runs: the function only prepares the
29
+ * first statement and examines the SQL after it.
30
+ */
31
+ export declare function hasSeveralStatements(db: Database, sql: string): boolean;
32
+ /**
33
+ * Execute multiple statements (for schema setup, etc.)
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * execMultiple(db, `
38
+ * CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT);
39
+ * CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY, user_id INTEGER);
40
+ * `);
41
+ * ```
42
+ */
43
+ export declare function execMultiple(db: Database, sql: string): void;
44
+ /**
45
+ * Get the last inserted row ID
46
+ */
47
+ export declare function lastInsertRowId(db: Database): number;
48
+ /**
49
+ * Check if a table exists
50
+ */
51
+ export declare function tableExists(db: Database, tableName: string): boolean;
52
+ //# sourceMappingURL=query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE7D;;;;;;;;;GASG;AACH,wBAAgB,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/C,EAAE,EAAE,QAAQ,EACZ,GAAG,EAAE,MAAM,EACX,MAAM,GAAE,OAAO,EAAO,GACrB,WAAW,CAAC,CAAC,CAAC,CAoBhB;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CACrB,EAAE,EAAE,QAAQ,EACZ,GAAG,EAAE,MAAM,EACX,MAAM,GAAE,OAAO,EAAO,GACrB,aAAa,CAiBf;AAiBD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAiBvE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAE5D;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,QAAQ,GAAG,MAAM,CAGpD;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAOpE"}
package/dist/query.js ADDED
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Query helper utilities for SQLite
3
+ */
4
+ /**
5
+ * Execute a SELECT query and return results
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const result = query(db, 'SELECT * FROM users WHERE age > ?', [21]);
10
+ * console.log(result.columns); // ['id', 'name', 'age']
11
+ * console.log(result.rows); // [{ id: 1, name: 'Alice', age: 25 }, ...]
12
+ * ```
13
+ */
14
+ export function query(db, sql, params = []) {
15
+ const stmt = db.prepare(sql);
16
+ try {
17
+ stmt.bind(params);
18
+ const rows = [];
19
+ let columns = [];
20
+ while (stmt.step()) {
21
+ if (columns.length === 0) {
22
+ columns = stmt.getColumnNames();
23
+ }
24
+ rows.push(stmt.getAsObject());
25
+ }
26
+ return { columns, rows };
27
+ }
28
+ finally {
29
+ stmt.free();
30
+ }
31
+ }
32
+ /**
33
+ * Execute an INSERT, UPDATE, DELETE, or other statement
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * const result = execute(db, 'INSERT INTO users (name, age) VALUES (?, ?)', ['Bob', 30]);
38
+ * console.log(result.changes); // 1
39
+ * ```
40
+ */
41
+ export function execute(db, sql, params = []) {
42
+ // With parameters, sql.js runs only the first statement. Before, the other statements were
43
+ // ignored and the call reported success. Now several statements run with no parameters, and
44
+ // are an error with parameters.
45
+ if (hasSeveralStatements(db, sql)) {
46
+ if (params.length > 0) {
47
+ throw new Error("execute() binds parameters to one statement, and the SQL has several. Send one statement per call.");
48
+ }
49
+ const before = totalChanges(db);
50
+ db.exec(sql);
51
+ return { changes: totalChanges(db) - before };
52
+ }
53
+ db.run(sql, params);
54
+ const changes = db.getRowsModified();
55
+ return { changes };
56
+ }
57
+ /** The number of rows that the statements of this connection changed. */
58
+ function totalChanges(db) {
59
+ const result = db.exec("SELECT total_changes()");
60
+ return Number(result[0]?.values[0]?.[0] ?? 0);
61
+ }
62
+ /** True when the text holds SQL other than spaces, ";" and comments. */
63
+ function holdsStatement(sql) {
64
+ return sql
65
+ .replace(/--[^\n]*/g, "")
66
+ .replace(/\/\*[\s\S]*?\*\//g, "")
67
+ .replace(/[\s;]/g, "")
68
+ .length > 0;
69
+ }
70
+ /**
71
+ * True when `sql` holds more than one statement. Nothing runs: the function only prepares the
72
+ * first statement and examines the SQL after it.
73
+ */
74
+ export function hasSeveralStatements(db, sql) {
75
+ const iterator = db.iterateStatements(sql);
76
+ const first = iterator.next();
77
+ if (first.done) {
78
+ return false;
79
+ }
80
+ const rest = iterator.getRemainingSQL();
81
+ // Read the iterator to its end: this frees the statement and the copy of the SQL. A later
82
+ // statement can fail to prepare (its table does not exist yet), and that also ends it.
83
+ try {
84
+ while (!iterator.next().done) {
85
+ // Prepare only
86
+ }
87
+ }
88
+ catch {
89
+ // The iterator ended
90
+ }
91
+ return holdsStatement(rest);
92
+ }
93
+ /**
94
+ * Execute multiple statements (for schema setup, etc.)
95
+ *
96
+ * @example
97
+ * ```typescript
98
+ * execMultiple(db, `
99
+ * CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT);
100
+ * CREATE TABLE IF NOT EXISTS posts (id INTEGER PRIMARY KEY, user_id INTEGER);
101
+ * `);
102
+ * ```
103
+ */
104
+ export function execMultiple(db, sql) {
105
+ db.exec(sql);
106
+ }
107
+ /**
108
+ * Get the last inserted row ID
109
+ */
110
+ export function lastInsertRowId(db) {
111
+ const result = query(db, "SELECT last_insert_rowid() as id");
112
+ return result.rows[0]?.id ?? 0;
113
+ }
114
+ /**
115
+ * Check if a table exists
116
+ */
117
+ export function tableExists(db, tableName) {
118
+ const result = query(db, "SELECT name FROM sqlite_master WHERE type='table' AND name=?", [tableName]);
119
+ return result.rows.length > 0;
120
+ }
121
+ //# sourceMappingURL=query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.js","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Type definitions for docker-sqlite
3
+ */
4
+ import type { Database } from "sql.js";
5
+ /**
6
+ * Configuration for SQLite connection
7
+ */
8
+ export interface ConnectionConfig {
9
+ /** Path to the SQLite database file. Use ":memory:" for in-memory database */
10
+ dbPath: string;
11
+ }
12
+ /**
13
+ * Callback function for withConnection
14
+ */
15
+ export type ConnectionCallback<T> = (db: Database) => T | Promise<T>;
16
+ /**
17
+ * Result of a query operation
18
+ */
19
+ export interface QueryResult<T = Record<string, unknown>> {
20
+ /** Column names */
21
+ columns: string[];
22
+ /** Row values */
23
+ rows: T[];
24
+ }
25
+ /**
26
+ * Result of an execute operation (INSERT, UPDATE, DELETE)
27
+ */
28
+ export interface ExecuteResult {
29
+ /** Number of rows affected */
30
+ changes: number;
31
+ }
32
+ /**
33
+ * Migration definition
34
+ */
35
+ export interface Migration {
36
+ /** Migration version (must be unique and sequential) */
37
+ version: number;
38
+ /** Description of the migration */
39
+ description: string;
40
+ /** SQL to apply the migration */
41
+ up: string;
42
+ /** SQL to revert the migration (optional) */
43
+ down?: string;
44
+ }
45
+ /**
46
+ * Migration status
47
+ */
48
+ export interface MigrationStatus {
49
+ /** Current version (0 if no migrations applied) */
50
+ currentVersion: number;
51
+ /** List of applied migrations */
52
+ appliedMigrations: number[];
53
+ /** List of pending migrations */
54
+ pendingMigrations: Migration[];
55
+ }
56
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAEvC;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,8EAA8E;IAC9E,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,MAAM,kBAAkB,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,QAAQ,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAErE;;GAEG;AACH,MAAM,WAAW,WAAW,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACtD,mBAAmB;IACnB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,iBAAiB;IACjB,IAAI,EAAE,CAAC,EAAE,CAAC;CACX;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAC;IAChB,mCAAmC;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,iCAAiC;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,mDAAmD;IACnD,cAAc,EAAE,MAAM,CAAC;IACvB,iCAAiC;IACjC,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,iCAAiC;IACjC,iBAAiB,EAAE,SAAS,EAAE,CAAC;CAChC"}
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Type definitions for docker-sqlite
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
@@ -0,0 +1,31 @@
1
+ # SQLite Docker Compose Configuration
2
+ #
3
+ # Provides a web UI for browsing SQLite databases.
4
+ #
5
+ # Environment variables (with defaults):
6
+ # SQLITE_CONTAINER_NAME - Container name (default: sqlite-web)
7
+ # SQLITE_PORT - Host port for web UI (default: 8080)
8
+ # SQLITE_DB_PATH - Path to database directory (default: ./data)
9
+ # SQLITE_DB_NAME - Database filename (default: cli.db)
10
+ #
11
+ # Usage:
12
+ # docker compose up -d
13
+ # docker compose down
14
+ # Open http://localhost:8080 to browse the database
15
+
16
+ services:
17
+ sqlite-web:
18
+ image: coleifer/sqlite-web
19
+ container_name: ${SQLITE_CONTAINER_NAME:-sqlite-web}
20
+ ports:
21
+ # Loopback only: the sqlite-web UI has full read/write access and no authentication
22
+ - "127.0.0.1:${SQLITE_PORT:-8080}:8080"
23
+ volumes:
24
+ - ${SQLITE_DB_PATH:-./data}:/data:rw
25
+ command: sqlite_web -H 0.0.0.0 /data/${SQLITE_DB_NAME:-cli.db}
26
+ restart: unless-stopped
27
+ healthcheck:
28
+ test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/"]
29
+ interval: 30s
30
+ timeout: 10s
31
+ retries: 3
package/package.json CHANGED
@@ -1,6 +1,86 @@
1
- {
2
- "name": "@mark1russell7/docker-sqlite",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "$schema": "https://json.schemastore.org/package",
3
+ "name": "@mark1russell7/docker-sqlite",
4
+ "version": "0.1.0",
5
+ "description": "SQLite for Node.js with no native build: sql.js (SQLite in WebAssembly) with safe file access (one call at a time per file, atomic saves, no write on read), queries, migrations and an optional Docker Compose web UI.",
6
+ "keywords": [
7
+ "sqlite",
8
+ "sqlite3",
9
+ "sql.js",
10
+ "wasm",
11
+ "webassembly",
12
+ "database",
13
+ "migrations",
14
+ "schema-migrations",
15
+ "query",
16
+ "embedded-database",
17
+ "docker",
18
+ "docker-compose",
19
+ "sqlite-web",
20
+ "typescript",
21
+ "node"
22
+ ],
23
+ "homepage": "https://github.com/mark1russell7/docker-sqlite#readme",
24
+ "bugs": {
25
+ "url": "https://github.com/mark1russell7/docker-sqlite/issues"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/mark1russell7/docker-sqlite.git"
30
+ },
31
+ "license": "MIT",
32
+ "author": "Mark Russell (https://github.com/mark1russell7)",
33
+ "type": "module",
34
+ "main": "./dist/index.js",
35
+ "types": "./dist/index.d.ts",
36
+ "exports": {
37
+ ".": {
38
+ "types": "./dist/index.d.ts",
39
+ "import": "./dist/index.js"
40
+ },
41
+ "./docker-compose.yml": "./docker-compose.yml",
42
+ "./schema/*": "./schema/*",
43
+ "./package.json": "./package.json"
44
+ },
45
+ "files": [
46
+ "dist",
47
+ "src",
48
+ "!dist/**/*.test.*",
49
+ "!src/**/*.test.ts",
50
+ "schema",
51
+ "docker-compose.yml",
52
+ "README.md",
53
+ "CHANGELOG.md",
54
+ "LICENSE"
55
+ ],
56
+ "scripts": {
57
+ "build": "tsc -b",
58
+ "typecheck": "tsc --noEmit",
59
+ "clean": "rm -rf dist .tsbuildinfo",
60
+ "prepare": "npm run build",
61
+ "test": "vitest run"
62
+ },
63
+ "dependencies": {
64
+ "@types/sql.js": "^1.4.11",
65
+ "sql.js": "^1.11.0"
66
+ },
67
+ "devDependencies": {
68
+ "@mark1russell7/cue": "github:mark1russell7/cue#main",
69
+ "@types/node": "^22.0.0",
70
+ "typescript": "^5.9.3",
71
+ "vitest": "^4.1.11"
72
+ },
73
+ "engines": {
74
+ "node": ">=22"
75
+ },
76
+ "sideEffects": false,
77
+ "publishConfig": {
78
+ "access": "public",
79
+ "provenance": true
80
+ },
81
+ "pnpm": {
82
+ "onlyBuiltDependencies": [
83
+ "@mark1russell7/cue"
84
+ ]
85
+ }
86
+ }
@@ -0,0 +1,21 @@
1
+ -- Log table schema for CLI logging
2
+ -- This is a reference schema, not applied automatically
3
+
4
+ CREATE TABLE IF NOT EXISTS logs (
5
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6
+ timestamp TEXT NOT NULL DEFAULT (datetime('now')),
7
+ level TEXT NOT NULL CHECK (level IN ('trace', 'debug', 'info', 'warn', 'error')),
8
+ message TEXT NOT NULL,
9
+ data TEXT, -- JSON serialized additional data
10
+ session_id TEXT, -- Groups logs by CLI session
11
+ command TEXT, -- CLI command that generated the log (e.g., 'lib.refresh')
12
+ context TEXT, -- Additional context (e.g., package name)
13
+ error_stack TEXT, -- Stack trace if error
14
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
15
+ );
16
+
17
+ -- Indexes for common queries
18
+ CREATE INDEX IF NOT EXISTS idx_logs_session ON logs(session_id);
19
+ CREATE INDEX IF NOT EXISTS idx_logs_level ON logs(level);
20
+ CREATE INDEX IF NOT EXISTS idx_logs_timestamp ON logs(timestamp);
21
+ CREATE INDEX IF NOT EXISTS idx_logs_command ON logs(command);
package/src/connect.ts ADDED
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Connection utilities for SQLite using sql.js
3
+ */
4
+
5
+ import initSqlJs, { type Database } from "sql.js";
6
+ import { readFile, writeFile, mkdir, rename, unlink } from "node:fs/promises";
7
+ import { dirname, resolve } from "node:path";
8
+ import type { ConnectionConfig, ConnectionCallback } from "./types.js";
9
+
10
+ let sqlPromise: Promise<Awaited<ReturnType<typeof initSqlJs>>> | null = null;
11
+
12
+ /**
13
+ * Initialize sql.js (cached)
14
+ */
15
+ async function getSql(): Promise<Awaited<ReturnType<typeof initSqlJs>>> {
16
+ if (!sqlPromise) {
17
+ sqlPromise = initSqlJs();
18
+ }
19
+ return sqlPromise;
20
+ }
21
+
22
+ /** A database and the bytes of its file when it was loaded (null: there was no file). */
23
+ interface LoadedDatabase {
24
+ db: Database;
25
+ original: Uint8Array | null;
26
+ }
27
+
28
+ /**
29
+ * Load a database from a file path or create a new one
30
+ */
31
+ async function loadDatabase(dbPath: string): Promise<LoadedDatabase> {
32
+ const SQL = await getSql();
33
+
34
+ if (dbPath === ":memory:") {
35
+ return { db: new SQL.Database(), original: null };
36
+ }
37
+
38
+ try {
39
+ const buffer = await readFile(dbPath);
40
+ // sql.js keeps the array that it gets and writes into it: give it a copy, so `original`
41
+ // keeps the bytes of the file
42
+ return { db: new SQL.Database(new Uint8Array(buffer)), original: buffer };
43
+ } catch (error) {
44
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
45
+ // File doesn't exist, create new database
46
+ return { db: new SQL.Database(), original: null };
47
+ }
48
+ throw error;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Save a database to a file path, when its content changed.
54
+ *
55
+ * A database that did not change is not written. Before, each call wrote the file, also a
56
+ * read: a read failed on a read-only file (EPERM), a read wrote an old copy over the rows of
57
+ * another process, and a query of a missing file made an empty file. The export of a
58
+ * database that did not change has the bytes of its file, and an empty new database exports
59
+ * no bytes.
60
+ *
61
+ * Writes a temporary file and renames it over the database, so a crash during the write
62
+ * cannot leave a truncated database (the rename replaces the file in one step).
63
+ */
64
+ async function saveDatabase(db: Database, dbPath: string, original: Uint8Array | null): Promise<Uint8Array | null> {
65
+ if (dbPath === ":memory:") {
66
+ return null;
67
+ }
68
+
69
+ const data = db.export();
70
+ const unchanged = original
71
+ ? Buffer.compare(Buffer.from(data), Buffer.from(original)) === 0
72
+ : data.length === 0;
73
+ if (unchanged) {
74
+ return original;
75
+ }
76
+ const buffer = Buffer.from(data);
77
+
78
+ // Ensure directory exists
79
+ await mkdir(dirname(dbPath), { recursive: true });
80
+
81
+ const tempPath = `${dbPath}.${process.pid}.${Date.now()}.tmp`;
82
+ await writeFile(tempPath, buffer);
83
+ try {
84
+ await rename(tempPath, dbPath);
85
+ } catch {
86
+ // For example, the file is held open on Windows: fall back to a direct write
87
+ await unlink(tempPath).catch(() => {});
88
+ await writeFile(dbPath, buffer);
89
+ }
90
+ return data;
91
+ }
92
+
93
+ /**
94
+ * The calls that wait for each database file. Each call reads the whole file, changes it in
95
+ * memory and writes it back, so two calls at once lost the changes of one of them
96
+ * (50 concurrent inserts kept 1 row). Calls for the same file now run one at a time in this
97
+ * process. (Other processes that write the same file are not coordinated.)
98
+ */
99
+ const fileQueues = new Map<string, Promise<unknown>>();
100
+
101
+ /**
102
+ * The queue key of a file. Windows file names do not depend on case, so the key is in lower
103
+ * case there. Before, two spellings of one file had two queues, and their calls ran at once.
104
+ */
105
+ export function fileQueueKey(dbPath: string, platform: NodeJS.Platform = process.platform): string {
106
+ const path = resolve(dbPath);
107
+ return platform === "win32" ? path.toLowerCase() : path;
108
+ }
109
+
110
+ async function inFileQueue<T>(dbPath: string, task: () => Promise<T>): Promise<T> {
111
+ if (dbPath === ":memory:") {
112
+ return task();
113
+ }
114
+ const key = fileQueueKey(dbPath);
115
+ const previous = fileQueues.get(key) ?? Promise.resolve();
116
+ const run = previous.then(task, task);
117
+ const tail = run.then(
118
+ () => undefined,
119
+ () => undefined
120
+ );
121
+ fileQueues.set(key, tail);
122
+ try {
123
+ return await run;
124
+ } finally {
125
+ if (fileQueues.get(key) === tail) {
126
+ fileQueues.delete(key);
127
+ }
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Execute a callback with a SQLite connection, ensuring proper cleanup and persistence.
133
+ *
134
+ * @example
135
+ * ```typescript
136
+ * const result = await withConnection(
137
+ * { dbPath: './data/app.db' },
138
+ * async (db) => {
139
+ * const stmt = db.prepare('SELECT * FROM users WHERE id = ?');
140
+ * stmt.bind([1]);
141
+ * const rows = [];
142
+ * while (stmt.step()) {
143
+ * rows.push(stmt.getAsObject());
144
+ * }
145
+ * stmt.free();
146
+ * return rows;
147
+ * }
148
+ * );
149
+ * ```
150
+ */
151
+ export async function withConnection<T>(
152
+ config: ConnectionConfig,
153
+ fn: ConnectionCallback<T>
154
+ ): Promise<T> {
155
+ return inFileQueue(config.dbPath, async () => {
156
+ const { db, original } = await loadDatabase(config.dbPath);
157
+
158
+ try {
159
+ const result = await fn(db);
160
+ // Save changes back to file (only when there are changes)
161
+ await saveDatabase(db, config.dbPath, original);
162
+ return result;
163
+ } finally {
164
+ db.close();
165
+ }
166
+ });
167
+ }
168
+
169
+ /**
170
+ * Create a SQLite database that can be manually managed.
171
+ * Caller is responsible for calling db.close() and saving if needed.
172
+ *
173
+ * @example
174
+ * ```typescript
175
+ * const { db, save } = await createConnection({ dbPath: './data/app.db' });
176
+ * try {
177
+ * db.run('INSERT INTO users (name) VALUES (?)', ['Alice']);
178
+ * await save();
179
+ * } finally {
180
+ * db.close();
181
+ * }
182
+ * ```
183
+ */
184
+ export async function createConnection(
185
+ config: ConnectionConfig
186
+ ): Promise<{ db: Database; save: () => Promise<void> }> {
187
+ const { db, original } = await loadDatabase(config.dbPath);
188
+ // The bytes of the file after the last save, to compare the next save with
189
+ let saved = original;
190
+ const save = async (): Promise<void> => {
191
+ saved = await saveDatabase(db, config.dbPath, saved);
192
+ };
193
+ return { db, save };
194
+ }
package/src/index.ts ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * docker-sqlite - Reusable SQLite Docker setup with TypeScript utilities
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * import { withConnection, query, execute, runMigrations } from 'docker-sqlite';
7
+ *
8
+ * // Define migrations
9
+ * const migrations = [
10
+ * {
11
+ * version: 1,
12
+ * description: 'Create users table',
13
+ * up: 'CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)',
14
+ * down: 'DROP TABLE users'
15
+ * }
16
+ * ];
17
+ *
18
+ * // Run with auto-save
19
+ * await withConnection({ dbPath: './data/app.db' }, (db) => {
20
+ * runMigrations(db, migrations);
21
+ *
22
+ * execute(db, 'INSERT INTO users (name, email) VALUES (?, ?)', ['Alice', 'alice@example.com']);
23
+ *
24
+ * const result = query(db, 'SELECT * FROM users');
25
+ * console.log(result.rows);
26
+ * });
27
+ * ```
28
+ */
29
+
30
+ // Types
31
+ export type {
32
+ ConnectionConfig,
33
+ ConnectionCallback,
34
+ QueryResult,
35
+ ExecuteResult,
36
+ Migration,
37
+ MigrationStatus,
38
+ } from "./types.js";
39
+
40
+ // Connection utilities
41
+ export { withConnection, createConnection } from "./connect.js";
42
+
43
+ // Query utilities
44
+ export {
45
+ query,
46
+ execute,
47
+ execMultiple,
48
+ lastInsertRowId,
49
+ tableExists,
50
+ } from "./query.js";
51
+
52
+ // Migration utilities
53
+ export {
54
+ getMigrationStatus,
55
+ runMigrations,
56
+ rollbackMigration,
57
+ createMigration,
58
+ } from "./migrate.js";
59
+
60
+ // Re-export sql.js Database type for convenience
61
+ export type { Database } from "sql.js";