tina4-nodejs 3.13.73 → 3.13.75
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/CLAUDE.md +2 -2
- package/package.json +1 -1
- package/packages/core/src/devAdmin.ts +18 -15
- package/packages/core/src/static.ts +74 -1
package/CLAUDE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.
|
|
1
|
+
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.75)
|
|
2
2
|
|
|
3
3
|
> This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
|
|
4
4
|
|
|
5
5
|
## What This Project Is
|
|
6
6
|
|
|
7
|
-
Tina4 for Node.js/TypeScript v3.13.
|
|
7
|
+
Tina4 for Node.js/TypeScript v3.13.75 - The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
|
|
8
8
|
|
|
9
9
|
The philosophy: zero ceremony, batteries included, file system as source of truth.
|
|
10
10
|
|
package/package.json
CHANGED
|
@@ -1753,30 +1753,33 @@ const handleConnectionsTest: RouteHandler = async (req, res) => {
|
|
|
1753
1753
|
let version = "Connected";
|
|
1754
1754
|
let tableCount = 0;
|
|
1755
1755
|
try {
|
|
1756
|
-
|
|
1756
|
+
// getTables() is async (Promise<string[]>) and MUST be awaited — the
|
|
1757
|
+
// un-awaited call made tableCount always 0 (Array.isArray(Promise) is
|
|
1758
|
+
// false). Python (db.get_tables()) and Ruby (db.tables) were correct.
|
|
1759
|
+
const tables = await db.getTables();
|
|
1757
1760
|
tableCount = Array.isArray(tables) ? tables.length : 0;
|
|
1758
1761
|
} catch { tableCount = 0; }
|
|
1759
1762
|
try {
|
|
1760
1763
|
const urlLower = url.toLowerCase();
|
|
1761
|
-
//
|
|
1762
|
-
//
|
|
1763
|
-
//
|
|
1764
|
-
//
|
|
1764
|
+
// fetchOne() is async and returns a single row object ({ v: ... }); it
|
|
1765
|
+
// must be awaited. Previously db.execute() was left un-awaited, so `row`
|
|
1766
|
+
// was a Promise and the version always fell back to the default string.
|
|
1767
|
+
// Mirrors Python/Ruby which read row["v"] from a fetch_one().
|
|
1765
1768
|
if (urlLower.includes("sqlite")) {
|
|
1766
|
-
const row = db.
|
|
1767
|
-
version = `SQLite ${
|
|
1769
|
+
const row = await db.fetchOne<{ v?: string }>("SELECT sqlite_version() as v");
|
|
1770
|
+
version = `SQLite ${row?.v ?? ""}`;
|
|
1768
1771
|
} else if (urlLower.includes("postgres")) {
|
|
1769
|
-
const row = db.
|
|
1770
|
-
version = (
|
|
1772
|
+
const row = await db.fetchOne<{ v?: string }>("SELECT version() as v");
|
|
1773
|
+
version = (row?.v ?? "PostgreSQL").toString().split(",")[0];
|
|
1771
1774
|
} else if (urlLower.includes("mysql")) {
|
|
1772
|
-
const row = db.
|
|
1773
|
-
version = `MySQL ${
|
|
1775
|
+
const row = await db.fetchOne<{ v?: string }>("SELECT version() as v");
|
|
1776
|
+
version = `MySQL ${row?.v ?? ""}`;
|
|
1774
1777
|
} else if (urlLower.includes("mssql")) {
|
|
1775
|
-
const row = db.
|
|
1776
|
-
version = (
|
|
1778
|
+
const row = await db.fetchOne<{ v?: string }>("SELECT @@VERSION as v");
|
|
1779
|
+
version = (row?.v ?? "MSSQL").toString().split("\n")[0];
|
|
1777
1780
|
} else if (urlLower.includes("firebird")) {
|
|
1778
|
-
const row = db.
|
|
1779
|
-
version = `Firebird ${
|
|
1781
|
+
const row = await db.fetchOne<{ v?: string }>("SELECT rdb$get_context('SYSTEM', 'ENGINE_VERSION') as v FROM rdb$database");
|
|
1782
|
+
version = `Firebird ${row?.v ?? ""}`;
|
|
1780
1783
|
}
|
|
1781
1784
|
} catch { /* keep version as Connected */ }
|
|
1782
1785
|
db.close();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, statSync, type Stats } from "node:fs";
|
|
2
2
|
import { join, extname } from "node:path";
|
|
3
3
|
import type { Tina4Request, Tina4Response } from "./types.js";
|
|
4
4
|
|
|
@@ -55,6 +55,29 @@ export function tryServeStatic(
|
|
|
55
55
|
const ext = extname(filePath);
|
|
56
56
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
57
57
|
|
|
58
|
+
// Cheap validators from the file's size + mtime — no hashing needed. A weak
|
|
59
|
+
// ETag (W/) is correct here: two representations with the same size+mtime
|
|
60
|
+
// are treated as equivalent for caching. Last-Modified is second-resolution
|
|
61
|
+
// per HTTP.
|
|
62
|
+
const etag = `W/"${stat.size}-${stat.mtimeMs}"`;
|
|
63
|
+
const lastModified = stat.mtime.toUTCString();
|
|
64
|
+
|
|
65
|
+
// A static asset MAY be cached but MUST be revalidated before use, so a
|
|
66
|
+
// redeployed file reaches the browser on the next load without a manual hard
|
|
67
|
+
// refresh; an unchanged asset costs a cheap 304, not a re-download. These
|
|
68
|
+
// ride on both the 200 and the 304 so the client always has fresh
|
|
69
|
+
// validators. Parity with the Python master static handler.
|
|
70
|
+
res.raw.setHeader("Cache-Control", "no-cache, must-revalidate");
|
|
71
|
+
res.raw.setHeader("ETag", etag);
|
|
72
|
+
res.raw.setHeader("Last-Modified", lastModified);
|
|
73
|
+
|
|
74
|
+
// Conditional request → 304 Not Modified with no body.
|
|
75
|
+
if (isNotModified(req, etag, stat)) {
|
|
76
|
+
res.raw.statusCode = 304;
|
|
77
|
+
res.raw.end();
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
58
81
|
res.raw.setHeader("Content-Type", contentType);
|
|
59
82
|
res.raw.setHeader("Content-Length", stat.size);
|
|
60
83
|
res.raw.end(readFileSync(filePath));
|
|
@@ -63,3 +86,53 @@ export function tryServeStatic(
|
|
|
63
86
|
|
|
64
87
|
return false;
|
|
65
88
|
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Read a conditional-request header off the incoming request. Works with the
|
|
92
|
+
* real `IncomingMessage` (`req.headers`, already lower-cased by Node) AND with
|
|
93
|
+
* the hand-rolled request objects used in unit tests. Header names must be
|
|
94
|
+
* lower-case. Returns "" when absent.
|
|
95
|
+
*/
|
|
96
|
+
function conditionalHeader(req: Tina4Request, name: string): string {
|
|
97
|
+
const headers = (req as { headers?: Record<string, string | string[] | undefined> }).headers;
|
|
98
|
+
const value = headers?.[name];
|
|
99
|
+
if (Array.isArray(value)) return value[0] ?? "";
|
|
100
|
+
return value ?? "";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Answer whether the cached representation the client already holds is still
|
|
105
|
+
* current. `If-None-Match` wins over `If-Modified-Since` (RFC 7232 §3.3): when
|
|
106
|
+
* the client sends an entity tag we compare against that alone.
|
|
107
|
+
*/
|
|
108
|
+
function isNotModified(req: Tina4Request, etag: string, stat: Stats): boolean {
|
|
109
|
+
const ifNoneMatch = conditionalHeader(req, "if-none-match");
|
|
110
|
+
if (ifNoneMatch) {
|
|
111
|
+
return etagMatches(ifNoneMatch, etag);
|
|
112
|
+
}
|
|
113
|
+
const ifModifiedSince = conditionalHeader(req, "if-modified-since");
|
|
114
|
+
if (ifModifiedSince) {
|
|
115
|
+
const since = Date.parse(ifModifiedSince);
|
|
116
|
+
if (!Number.isNaN(since)) {
|
|
117
|
+
// Last-Modified is second-resolution; floor the mtime so sub-second
|
|
118
|
+
// precision never reports a spurious "modified".
|
|
119
|
+
const modified = Math.floor(stat.mtimeMs / 1000) * 1000;
|
|
120
|
+
return modified <= since;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Weak comparison of an `If-None-Match` value against our ETag. The header may
|
|
128
|
+
* be `*`, a single tag, or a comma-separated list; the `W/` weak prefix is
|
|
129
|
+
* stripped on both sides before comparing (RFC 7232 §2.3.2 weak comparison).
|
|
130
|
+
*/
|
|
131
|
+
function etagMatches(ifNoneMatch: string, etag: string): boolean {
|
|
132
|
+
const strip = (tag: string) => tag.trim().replace(/^W\//, "");
|
|
133
|
+
const target = strip(etag);
|
|
134
|
+
return ifNoneMatch.split(",").some((candidate) => {
|
|
135
|
+
const trimmed = candidate.trim();
|
|
136
|
+
return trimmed === "*" || strip(trimmed) === target;
|
|
137
|
+
});
|
|
138
|
+
}
|