tina4-nodejs 3.11.10 → 3.11.12

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
 
5
5
 
6
- "version": "3.11.10",
6
+ "version": "3.11.12",
7
7
 
8
8
  "type": "module",
9
9
  "description": "Tina4 for Node.js/TypeScript — 54 built-in features, zero dependencies",
@@ -1,17 +1,48 @@
1
1
  import { DatabaseSync } from "node:sqlite";
2
2
  import { mkdirSync } from "node:fs";
3
- import { dirname } from "node:path";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
4
4
  import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
5
5
 
6
+ /**
7
+ * Resolve a SQLite path argument against the project root (cwd).
8
+ *
9
+ * Matches the tina4-python + tina4-php convention:
10
+ * ":memory:" → passthrough
11
+ * "data/app.db" → {cwd}/data/app.db (auto-mkdir under cwd)
12
+ * "/abs/app.db" → /abs/app.db (NO auto-mkdir; user's responsibility)
13
+ * "C:/Users/app.db" → C:/Users/app.db (NO auto-mkdir)
14
+ *
15
+ * Never mkdir a directory that isn't a descendant of cwd — that was the
16
+ * root cause of the `EROFS: read-only file system, mkdir '/data'` crash
17
+ * reported on macOS.
18
+ */
19
+ function resolveSqlitePath(dbPath: string): string {
20
+ if (dbPath === ":memory:") return dbPath;
21
+
22
+ let path = dbPath;
23
+ if (!isAbsolute(path)) {
24
+ path = join(process.cwd(), path);
25
+ // Auto-mkdir is safe here — we know the parent is under cwd
26
+ mkdirSync(dirname(path), { recursive: true });
27
+ } else {
28
+ // Absolute path. Only auto-mkdir if it's a descendant of cwd.
29
+ const cwd = resolve(process.cwd());
30
+ const abs = resolve(path);
31
+ if (abs.startsWith(cwd + "/") || abs === cwd) {
32
+ mkdirSync(dirname(abs), { recursive: true });
33
+ }
34
+ // Otherwise, trust the user — don't touch the filesystem.
35
+ }
36
+ return path;
37
+ }
38
+
6
39
  export class SQLiteAdapter implements DatabaseAdapter {
7
40
  private db: DatabaseSync;
8
41
  private _lastInsertId: number | bigint | null = null;
9
42
 
10
43
  constructor(dbPath: string) {
11
- if (dbPath !== ":memory:") {
12
- mkdirSync(dirname(dbPath), { recursive: true });
13
- }
14
- this.db = new DatabaseSync(dbPath);
44
+ const resolved = resolveSqlitePath(dbPath);
45
+ this.db = new DatabaseSync(resolved);
15
46
  this.db.exec("PRAGMA journal_mode = WAL");
16
47
  this.db.exec("PRAGMA foreign_keys = ON");
17
48
  }
@@ -92,18 +92,31 @@ export interface ParsedDatabaseUrl {
92
92
  export function parseDatabaseUrl(url: string, username?: string, password?: string): ParsedDatabaseUrl {
93
93
  let result: ParsedDatabaseUrl;
94
94
 
95
- // Handle sqlite:// separately because URL class mangles the path
96
- if (url.startsWith("sqlite:///")) {
97
- // sqlite:///absolute/path three slashes means absolute
98
- let path = url.slice("sqlite://".length);
99
- // Windows: sqlite:///C:/Users/app.db /C:/Users/app.db after slicing.
100
- // The leading / before the drive letter must be removed.
101
- if (/^\/[A-Za-z]:/.test(path)) {
102
- path = path.slice(1);
103
- }
104
- result = { type: "sqlite", path };
95
+ // Handle sqlite:// separately because URL class mangles the path.
96
+ //
97
+ // Convention (matches tina4-python, tina4-php, and the docs):
98
+ // sqlite::memory: → in-memory
99
+ // sqlite:///:memory: in-memory (URL form)
100
+ // sqlite:///app.db → ./app.db (relative to cwd)
101
+ // sqlite:///data/app.db → ./data/app.db (relative)
102
+ // sqlite:////absolute/app.db → /absolute/app.db (absolute)
103
+ // sqlite:///C:/Users/app.db → C:/Users/app.db (Windows absolute)
104
+ if (url === "sqlite::memory:" || url === "sqlite:///:memory:") {
105
+ result = { type: "sqlite", path: ":memory:" };
106
+ } else if (url.startsWith("sqlite:///")) {
107
+ // Strip the "sqlite://" prefix (leaving one "/" + path)
108
+ let rest = url.slice("sqlite://".length); // e.g. "/data/app.db" or "//abs/app.db" or "/C:/Users/..."
109
+ // Drop exactly one leading "/"
110
+ if (rest.startsWith("/")) rest = rest.slice(1);
111
+ // Windows absolute: C:/Users/app.db or C:\...
112
+ const isWindowsAbs = /^[A-Za-z]:[\/\\]/.test(rest);
113
+ // Unix absolute: still starts with "/" after the strip (four-slash URL form)
114
+ const isUnixAbs = rest.startsWith("/");
115
+ result = { type: "sqlite", path: isWindowsAbs || isUnixAbs ? rest : rest };
116
+ // Relative paths are resolved against cwd by the SQLite adapter at connect time;
117
+ // keep the string as-is here so tests can inspect the raw form.
105
118
  } else if (url.startsWith("sqlite://")) {
106
- // sqlite://./relative or sqlite://relative
119
+ // sqlite://./relative or sqlite://relative — legacy two-slash form
107
120
  const path = url.slice("sqlite://".length);
108
121
  result = { type: "sqlite", path };
109
122
  } else if (url.startsWith("mssql://") || url.startsWith("sqlserver://")) {