tina4-nodejs 3.13.81 → 3.13.83
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 +6 -6
- package/README.md +4 -4
- package/package.json +1 -1
- package/packages/cli/src/bin.ts +163 -13
- package/packages/core/public/css/tina4.css +56 -130
- package/packages/core/public/css/tina4.min.css +1 -1
- package/packages/core/src/devAdmin.ts +9 -11
- package/packages/core/src/index.ts +4 -0
- package/packages/core/src/mqtt.ts +859 -0
- package/packages/core/src/mqttMessage.ts +104 -0
- package/packages/core/src/scss.ts +48 -1
- package/packages/core/src/server.ts +98 -9
- package/packages/core/src/testClient.ts +72 -7
- package/packages/orm/src/adapters/firebird.ts +1 -1
- package/packages/orm/src/adapters/mssql.ts +1 -1
- package/packages/orm/src/adapters/mysql.ts +1 -1
- package/packages/orm/src/adapters/postgres.ts +1 -1
- package/packages/orm/src/adapters/sqlite.ts +1 -1
- package/packages/orm/src/baseModel.ts +1 -1
- package/packages/orm/src/cachedDatabase.ts +1 -1
- package/packages/orm/src/database.ts +1 -1
- package/packages/orm/src/index.ts +1 -1
- /package/packages/orm/src/{sqlTranslation.ts → sqlTranslator.ts} +0 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One MQTT 3.1.1 application message as delivered by the broker.
|
|
3
|
+
*
|
|
4
|
+
* Shaped like the Queue's job (its unit of work): it carries the payload plus
|
|
5
|
+
* the delivery metadata a consumer needs, and it knows how to acknowledge itself
|
|
6
|
+
* back to the client it came from. Mirrors tina4_python.mqtt.MqttMessage,
|
|
7
|
+
* Tina4::MqttMessage (Ruby), and Tina4\MqttMessage (PHP).
|
|
8
|
+
*
|
|
9
|
+
* The two flags matter for correctness, not decoration:
|
|
10
|
+
*
|
|
11
|
+
* retained — the broker replayed the topic's last known value to us because
|
|
12
|
+
* we subscribed AFTER it was published. It is current state, not a
|
|
13
|
+
* fresh event.
|
|
14
|
+
* duplicate — the DUP flag. The broker is REDELIVERING a QoS 1 message it never
|
|
15
|
+
* saw acknowledged. QoS 1 is at-least-once, so a duplicate is
|
|
16
|
+
* guaranteed eventually; a consumer that treats a DUP delivery as a
|
|
17
|
+
* new sample double-counts energy or mileage. Key the ingest on
|
|
18
|
+
* (deviceId, deviceTimestamp) and it is harmless.
|
|
19
|
+
*
|
|
20
|
+
* The payload is a Buffer of the raw bytes; text() decodes it for JSON/string
|
|
21
|
+
* payloads.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** The slice of an Mqtt client an MqttMessage needs to acknowledge itself. */
|
|
25
|
+
export interface MqttAcknowledger {
|
|
26
|
+
acknowledge(packetId: number): Promise<boolean>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class MqttMessage {
|
|
30
|
+
private acknowledgedFlag = false;
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
public readonly topic: string,
|
|
34
|
+
public readonly payload: Buffer,
|
|
35
|
+
public readonly qos: number = 0,
|
|
36
|
+
public readonly retained: boolean = false,
|
|
37
|
+
public readonly duplicate: boolean = false,
|
|
38
|
+
public readonly packetId: number | null = null,
|
|
39
|
+
private readonly client: MqttAcknowledger | null = null,
|
|
40
|
+
) {}
|
|
41
|
+
|
|
42
|
+
/** True when the broker replayed this as the topic's retained (last known) value. */
|
|
43
|
+
isRetained(): boolean {
|
|
44
|
+
return this.retained;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* True when the broker set the DUP flag — a REDELIVERY of a QoS 1 message we
|
|
49
|
+
* never acknowledged, not a new sample.
|
|
50
|
+
*/
|
|
51
|
+
isDuplicate(): boolean {
|
|
52
|
+
return this.duplicate;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* PUBACK a QoS 1 delivery so the broker stops redelivering it.
|
|
57
|
+
*
|
|
58
|
+
* A QoS 0 message needs no acknowledgement, and a second call is a no-op, so
|
|
59
|
+
* this is always safe to call once processing succeeded. Returns true only
|
|
60
|
+
* when a PUBACK was actually sent.
|
|
61
|
+
*/
|
|
62
|
+
async acknowledge(): Promise<boolean> {
|
|
63
|
+
if (this.acknowledgedFlag || this.qos === 0 || this.packetId === null || this.client === null) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
await this.client.acknowledge(this.packetId);
|
|
67
|
+
this.acknowledgedFlag = true;
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** True once this message has been acknowledged back to the broker. */
|
|
72
|
+
isAcknowledged(): boolean {
|
|
73
|
+
return this.acknowledgedFlag;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The payload decoded as text (for JSON / string payloads). */
|
|
77
|
+
text(encoding: BufferEncoding = "utf-8"): string {
|
|
78
|
+
return this.payload.toString(encoding);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The message as a plain object. */
|
|
82
|
+
toObject(): {
|
|
83
|
+
topic: string;
|
|
84
|
+
payload: Buffer;
|
|
85
|
+
qos: number;
|
|
86
|
+
retained: boolean;
|
|
87
|
+
duplicate: boolean;
|
|
88
|
+
packetId: number | null;
|
|
89
|
+
} {
|
|
90
|
+
return {
|
|
91
|
+
topic: this.topic,
|
|
92
|
+
payload: this.payload,
|
|
93
|
+
qos: this.qos,
|
|
94
|
+
retained: this.retained,
|
|
95
|
+
duplicate: this.duplicate,
|
|
96
|
+
packetId: this.packetId,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** String form is the payload text (parity with Python __str__ / Ruby to_s). */
|
|
101
|
+
toString(): string {
|
|
102
|
+
return this.text();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -168,9 +168,56 @@ function resolveImports(content: string, paths: string[], imported: Set<string>)
|
|
|
168
168
|
|
|
169
169
|
// ── Variables ────────────────────────────────────────────────────
|
|
170
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Flags that may trail a variable declaration's value. `!default` means "assign
|
|
173
|
+
* only if this variable is not already set" — the flag that makes a variable
|
|
174
|
+
* themeable. `!global` is the scope flag. Both are compiler directives: they are
|
|
175
|
+
* consumed at the declaration and must never reach the CSS, because
|
|
176
|
+
* `padding: 1.5rem !default` is invalid CSS and browsers drop the whole
|
|
177
|
+
* declaration. Sass flag names are case-SENSITIVE (`!DEFAULT` is an error in
|
|
178
|
+
* Dart Sass), so the match is deliberately case-sensitive.
|
|
179
|
+
*/
|
|
180
|
+
const VARIABLE_FLAG = /\s*!(default|global)\s*$/;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Split trailing `!default` / `!global` flags off a variable declaration value.
|
|
184
|
+
* Returns `[valueWithoutFlags, declaresDefault]`.
|
|
185
|
+
*
|
|
186
|
+
* Only ever called on the value of a `$name: value;` declaration, so a literal
|
|
187
|
+
* `!default` anywhere else — inside a quoted string (`content: "x !default y"`)
|
|
188
|
+
* or a function argument — is left untouched, exactly as Dart Sass leaves it. A
|
|
189
|
+
* blanket strip would corrupt real string content, and would silently turn
|
|
190
|
+
* `rgba(#000 !default, 0.1)` (a syntax error in Dart Sass) into valid-looking
|
|
191
|
+
* CSS that Sass would never emit.
|
|
192
|
+
*/
|
|
193
|
+
function stripVariableFlags(value: string): [string, boolean] {
|
|
194
|
+
let declaresDefault = false;
|
|
195
|
+
for (;;) {
|
|
196
|
+
const match = VARIABLE_FLAG.exec(value);
|
|
197
|
+
if (match === null) return [value.trim(), declaresDefault];
|
|
198
|
+
if (match[1] === "default") declaresDefault = true;
|
|
199
|
+
value = value.slice(0, match.index);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Extract `$variable: value;` declarations, honouring the `!default` flag.
|
|
205
|
+
*
|
|
206
|
+
* `$x: value !default;` assigns only when `$x` is not already set. That is what
|
|
207
|
+
* makes a variable themeable — a user who writes `$primary: red;` BEFORE
|
|
208
|
+
* importing a partial that declares `$primary: blue !default;` keeps red.
|
|
209
|
+
* Declarations are visited in source order, so "already set" means "set by an
|
|
210
|
+
* earlier declaration or by a preset variable". A value of `null` counts as
|
|
211
|
+
* unset, as in Sass.
|
|
212
|
+
*/
|
|
171
213
|
function extractVariables(scss: string, variables: Record<string, string>): string {
|
|
172
214
|
return scss.replace(/\$([a-zA-Z_][\w-]*)\s*:\s*([^;]+);/g, (_m, name: string, value: string) => {
|
|
173
|
-
|
|
215
|
+
const [stripped, declaresDefault] = stripVariableFlags(value.trim());
|
|
216
|
+
// !default must not overwrite a value that is already set.
|
|
217
|
+
if (declaresDefault && (variables[name] ?? "null") !== "null") {
|
|
218
|
+
return "";
|
|
219
|
+
}
|
|
220
|
+
let resolved = stripped;
|
|
174
221
|
// Resolve variable references within the value
|
|
175
222
|
for (const [vName, vVal] of Object.entries(variables)) {
|
|
176
223
|
resolved = resolved.replaceAll(`$${vName}`, vVal);
|
|
@@ -34,6 +34,73 @@ const BUILTIN_ERROR_TEMPLATES_DIR = resolve(__dirname, "..", "templates");
|
|
|
34
34
|
/** Built-in public directory for framework-bundled static assets. */
|
|
35
35
|
const BUILTIN_PUBLIC_DIR = resolve(__dirname, "..", "public");
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Whether the framework's bundled Swagger UI assets (public/swagger/*) may be
|
|
39
|
+
* served.
|
|
40
|
+
*
|
|
41
|
+
* Static files are resolved BEFORE routes, so the shipped
|
|
42
|
+
* public/swagger/index.html answered `GET /swagger` even when
|
|
43
|
+
* `swaggerEnabled()` was false -- serving the Swagger UI in production and
|
|
44
|
+
* bypassing the documented TINA4_SWAGGER_ENABLED / TINA4_DEBUG gate entirely.
|
|
45
|
+
* The symptom was a 200 on /swagger with a 404 on /swagger/openapi.json (the
|
|
46
|
+
* gated route never registered, the static file still won).
|
|
47
|
+
*
|
|
48
|
+
* Set from `swaggerEnabled()` at boot. Stays false when swagger is disabled OR
|
|
49
|
+
* when the swagger module fails to load -- fail closed, never expose.
|
|
50
|
+
*/
|
|
51
|
+
let swaggerAssetsEnabled = false;
|
|
52
|
+
|
|
53
|
+
/** Bundled Swagger UI asset paths that must honour the swagger gate. */
|
|
54
|
+
function isSwaggerAssetPath(pathname: string): boolean {
|
|
55
|
+
return pathname === "/swagger" || pathname.startsWith("/swagger/");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Build the startup banner's optional surface lines (issue #99).
|
|
60
|
+
*
|
|
61
|
+
* Only advertise a surface that is actually REACHABLE. In production, or with
|
|
62
|
+
* TINA4_DEBUG off, /swagger and /__dev return 404 -- printing them anyway both
|
|
63
|
+
* misleads an operator into believing a dev surface is exposed and sends a
|
|
64
|
+
* developer to a dead link.
|
|
65
|
+
*
|
|
66
|
+
* Kept as a pure function of (port, two booleans) so the contract is unit
|
|
67
|
+
* testable without booting a server and grepping stdout. Parity: Python
|
|
68
|
+
* banner_surface_lines, PHP App::bannerSurfaceLines, Ruby
|
|
69
|
+
* Tina4.banner_surface_lines.
|
|
70
|
+
*
|
|
71
|
+
* @returns [swaggerLine, dashboardLine] -- each empty, or a newline plus the
|
|
72
|
+
* banner row, ready to interpolate.
|
|
73
|
+
*/
|
|
74
|
+
export function bannerSurfaceLines(
|
|
75
|
+
port: number,
|
|
76
|
+
opts: { swaggerEnabled: boolean; devAdminEnabled: boolean },
|
|
77
|
+
): [string, string] {
|
|
78
|
+
return [
|
|
79
|
+
opts.swaggerEnabled ? `\n Swagger: http://localhost:${port}/swagger` : "",
|
|
80
|
+
opts.devAdminEnabled ? `\n Dashboard: http://localhost:${port}/__dev` : "",
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Whether the startup banner should ADVERTISE /swagger (issue #99).
|
|
86
|
+
*
|
|
87
|
+
* Mirrors `swaggerEnabled()` in packages/swagger/src/ui.ts: an explicit
|
|
88
|
+
* TINA4_SWAGGER_ENABLED wins, otherwise fall back to TINA4_DEBUG.
|
|
89
|
+
*
|
|
90
|
+
* Read from env here rather than importing the swagger package, because the
|
|
91
|
+
* CLUSTER PRIMARY prints its banner before any optional module is loaded (and a
|
|
92
|
+
* dynamic import for one banner line is not worth the boot cost). Keep this in
|
|
93
|
+
* sync with ui.ts -- it is the same two-line contract.
|
|
94
|
+
*/
|
|
95
|
+
function swaggerAdvertised(): boolean {
|
|
96
|
+
const TRUTHY = ["true", "1", "yes", "on"];
|
|
97
|
+
const raw = (process.env.TINA4_SWAGGER_ENABLED ?? "").trim().toLowerCase();
|
|
98
|
+
if (raw === "") {
|
|
99
|
+
return TRUTHY.includes((process.env.TINA4_DEBUG ?? "").trim().toLowerCase());
|
|
100
|
+
}
|
|
101
|
+
return TRUTHY.includes(raw);
|
|
102
|
+
}
|
|
103
|
+
|
|
37
104
|
/**
|
|
38
105
|
* Apply pending DB migrations on startup — NON-BREAKING.
|
|
39
106
|
*
|
|
@@ -787,6 +854,15 @@ export async function startServer(config?: Tina4Config): Promise<{
|
|
|
787
854
|
const logLevel = (process.env.TINA4_LOG_LEVEL ?? "DEBUG").toUpperCase();
|
|
788
855
|
|
|
789
856
|
if (!isBannerSuppressed()) {
|
|
857
|
+
// Only advertise a surface that is actually reachable (issue #99).
|
|
858
|
+
// Cluster mode is the production path: debug is OFF, so /__dev always
|
|
859
|
+
// 404s here and is never advertised; /swagger only when explicitly on.
|
|
860
|
+
// Cluster mode is the production path: debug is OFF, so /__dev never
|
|
861
|
+
// advertises here.
|
|
862
|
+
const [swaggerLine] = bannerSurfaceLines(port, {
|
|
863
|
+
swaggerEnabled: swaggerAdvertised(),
|
|
864
|
+
devAdminEnabled: false,
|
|
865
|
+
});
|
|
790
866
|
console.log(`${color}
|
|
791
867
|
______ _ __ __
|
|
792
868
|
/_ __/(_)___ ____ _/ // /
|
|
@@ -796,9 +872,7 @@ export async function startServer(config?: Tina4Config): Promise<{
|
|
|
796
872
|
${reset}
|
|
797
873
|
Tina4 Node.js v${TINA4_VERSION} — The Intelligent Native Application 4ramework
|
|
798
874
|
|
|
799
|
-
Server: http://${displayHost}:${port} (cluster, ${numCPUs} workers)
|
|
800
|
-
Swagger: http://localhost:${port}/swagger
|
|
801
|
-
Dashboard: http://localhost:${port}/__dev
|
|
875
|
+
Server: http://${displayHost}:${port} (cluster, ${numCPUs} workers)${swaggerLine}
|
|
802
876
|
Debug: OFF (Log level: ${logLevel})
|
|
803
877
|
`);
|
|
804
878
|
}
|
|
@@ -1000,7 +1074,10 @@ ${reset}
|
|
|
1000
1074
|
// when disabled.
|
|
1001
1075
|
try {
|
|
1002
1076
|
const swagger = await import("../../swagger/src/index.js");
|
|
1003
|
-
|
|
1077
|
+
// Single source of truth for BOTH the gated routes and the bundled
|
|
1078
|
+
// public/swagger assets (which static serving would otherwise expose).
|
|
1079
|
+
swaggerAssetsEnabled = swagger.swaggerEnabled();
|
|
1080
|
+
if (!swaggerAssetsEnabled) {
|
|
1004
1081
|
// Skip the rest of the swagger block when disabled.
|
|
1005
1082
|
throw new Error("__swagger_disabled__");
|
|
1006
1083
|
}
|
|
@@ -1273,8 +1350,14 @@ ${reset}
|
|
|
1273
1350
|
if (existsSync(srcPublicDir) && tryServeStatic(srcPublicDir, req, res)) {
|
|
1274
1351
|
return;
|
|
1275
1352
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1353
|
+
// Framework-bundled assets. The Swagger UI lives here (public/swagger/),
|
|
1354
|
+
// and static files resolve BEFORE routes -- so this path MUST honour the
|
|
1355
|
+
// swagger gate or /swagger is served in production regardless of
|
|
1356
|
+
// TINA4_SWAGGER_ENABLED / TINA4_DEBUG.
|
|
1357
|
+
if (swaggerAssetsEnabled || !isSwaggerAssetPath(pathname)) {
|
|
1358
|
+
if (tryServeStatic(BUILTIN_PUBLIC_DIR, req, res)) {
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1278
1361
|
}
|
|
1279
1362
|
|
|
1280
1363
|
// Match route
|
|
@@ -1602,6 +1685,14 @@ ${reset}
|
|
|
1602
1685
|
: "";
|
|
1603
1686
|
|
|
1604
1687
|
if (!isBannerSuppressed()) {
|
|
1688
|
+
// Only advertise a surface that is actually reachable (issue #99). With
|
|
1689
|
+
// debug off / in production these endpoints 404, and printing a dead URL
|
|
1690
|
+
// both misleads an operator into believing a dev surface is exposed and
|
|
1691
|
+
// sends a developer to a 404.
|
|
1692
|
+
const [swaggerLine, dashboardLine] = bannerSurfaceLines(port, {
|
|
1693
|
+
swaggerEnabled: swaggerAdvertised(),
|
|
1694
|
+
devAdminEnabled: isDebug,
|
|
1695
|
+
});
|
|
1605
1696
|
console.log(`${color}
|
|
1606
1697
|
______ _ __ __
|
|
1607
1698
|
/_ __/(_)___ ____ _/ // /
|
|
@@ -1611,9 +1702,7 @@ ${reset}
|
|
|
1611
1702
|
${reset}
|
|
1612
1703
|
Tina4 Node.js v${TINA4_VERSION} — The Intelligent Native Application 4ramework
|
|
1613
1704
|
|
|
1614
|
-
Server: http://${displayHost}:${port} (${serverMode})
|
|
1615
|
-
Swagger: http://localhost:${port}/swagger
|
|
1616
|
-
Dashboard: http://localhost:${port}/__dev
|
|
1705
|
+
Server: http://${displayHost}:${port} (${serverMode})${swaggerLine}${dashboardLine}
|
|
1617
1706
|
Debug: ${isDebug ? "ON" : "OFF"} (Log level: ${logLevel})${dualPortLines}
|
|
1618
1707
|
`);
|
|
1619
1708
|
}
|
|
@@ -18,7 +18,8 @@ import { IncomingMessage, ServerResponse } from "node:http";
|
|
|
18
18
|
import { Socket } from "node:net";
|
|
19
19
|
import { createRequest } from "./request.js";
|
|
20
20
|
import { createResponse } from "./response.js";
|
|
21
|
-
import { defaultRouter,
|
|
21
|
+
import { defaultRouter, Router, runRouteMiddlewares } from "./router.js";
|
|
22
|
+
import { MiddlewareRunner } from "./middleware.js";
|
|
22
23
|
import { enforceRouteAuth } from "./authGate.js";
|
|
23
24
|
|
|
24
25
|
export class TestResponse {
|
|
@@ -160,14 +161,73 @@ export class TestClient {
|
|
|
160
161
|
const cleanPath = path.includes("?") ? path.split("?")[0] : path;
|
|
161
162
|
|
|
162
163
|
// Match route
|
|
163
|
-
const
|
|
164
|
+
const httpMethod = method.toUpperCase();
|
|
165
|
+
const match = this.router.match(httpMethod, cleanPath);
|
|
164
166
|
if (!match) {
|
|
165
|
-
|
|
167
|
+
// D6: dispatch through the REAL front-controller behaviour on a miss —
|
|
168
|
+
// never fabricate a body. This mirrors the live server tail
|
|
169
|
+
// (server.ts dispatch): RFC 9110 conformance first (a path registered
|
|
170
|
+
// under another method answers OPTIONS with 204 + Allow and any other
|
|
171
|
+
// method with 405 + Allow), then the framework's real 404. The old
|
|
172
|
+
// TestClient short-circuited to a hand-invented {"error":"Not found"}
|
|
173
|
+
// that the live server never sends, so a green test proved nothing about
|
|
174
|
+
// production (the same silent-success class as Python's pre-fix client).
|
|
175
|
+
const allowed = this.router.methodsAllowedForPath(cleanPath);
|
|
176
|
+
if (allowed.length > 0) {
|
|
177
|
+
const allowHeader = allowed.join(", ");
|
|
178
|
+
if (httpMethod === "OPTIONS") {
|
|
179
|
+
return new TestResponse(204, { allow: allowHeader, "content-length": "0" }, "");
|
|
180
|
+
}
|
|
181
|
+
const body405 = JSON.stringify({
|
|
182
|
+
error: "Method Not Allowed",
|
|
183
|
+
path: cleanPath,
|
|
184
|
+
method: httpMethod,
|
|
185
|
+
allow: allowed,
|
|
186
|
+
statusCode: 405,
|
|
187
|
+
});
|
|
188
|
+
return new TestResponse(405, { allow: allowHeader, "content-type": "application/json" }, body405);
|
|
189
|
+
}
|
|
190
|
+
// The framework's real 404. The live server renders an HTML error page
|
|
191
|
+
// when a project templatesDir/frondEngine exists and otherwise emits this
|
|
192
|
+
// exact JSON; a server-less TestClient has no project dir, so it produces
|
|
193
|
+
// the JSON fallback the live front controller itself falls back to.
|
|
194
|
+
// tina4: HTML-error-page + filesystem static serving are the only live
|
|
195
|
+
// front-controller steps a server-less client can't reproduce.
|
|
196
|
+
const body404 = JSON.stringify({
|
|
197
|
+
error: "Not Found",
|
|
198
|
+
statusCode: 404,
|
|
199
|
+
message: `No route found for ${httpMethod} ${cleanPath}`,
|
|
200
|
+
});
|
|
201
|
+
return new TestResponse(404, { "content-type": "application/json" }, body404);
|
|
166
202
|
}
|
|
167
203
|
|
|
168
204
|
// Inject route params
|
|
169
205
|
req.params = match.params;
|
|
170
206
|
|
|
207
|
+
// Global class-based middleware (Router.use / MiddlewareRunner.use), run in
|
|
208
|
+
// the SAME order the live dispatcher uses (server.ts): beforeX hooks before
|
|
209
|
+
// the handler, afterX hooks after — even on a before-short-circuit. Empty
|
|
210
|
+
// when a test registers none, so this is additive/non-breaking.
|
|
211
|
+
const globalMiddleware = [
|
|
212
|
+
...new Set([...Router.getClassMiddlewares(), ...MiddlewareRunner.getGlobal()]),
|
|
213
|
+
];
|
|
214
|
+
if (globalMiddleware.length > 0) {
|
|
215
|
+
const [, , proceed] = await MiddlewareRunner.runBefore(globalMiddleware, req, res);
|
|
216
|
+
if (!proceed || res.raw.writableEnded) {
|
|
217
|
+
await MiddlewareRunner.runAfter(globalMiddleware, req, res);
|
|
218
|
+
if (!res.raw.writableEnded) res.raw.end();
|
|
219
|
+
return this._collect(rawRes, chunks, socket);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Per-route middleware, same as the live dispatcher.
|
|
224
|
+
if (match.middlewares && match.middlewares.length > 0) {
|
|
225
|
+
const proceed = await runRouteMiddlewares(match.middlewares, req, res);
|
|
226
|
+
if (!proceed || res.raw.writableEnded) {
|
|
227
|
+
return this._collect(rawRes, chunks, socket);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
171
231
|
// Route through the REAL auth gate (parity with the live server). A write to
|
|
172
232
|
// an auth-required route (secure-by-default POST/PUT/PATCH/DELETE, or any
|
|
173
233
|
// .secure() route) with no valid token / formToken / session token 401s here
|
|
@@ -183,9 +243,17 @@ export class TestClient {
|
|
|
183
243
|
// Execute handler (only if auth passed)
|
|
184
244
|
if (!rejected) {
|
|
185
245
|
await match.handler(req, res);
|
|
246
|
+
// Global afterX hooks (logging / post-processing), mirroring the live tail.
|
|
247
|
+
if (globalMiddleware.length > 0) {
|
|
248
|
+
await MiddlewareRunner.runAfter(globalMiddleware, req, res);
|
|
249
|
+
}
|
|
186
250
|
}
|
|
187
251
|
|
|
188
|
-
|
|
252
|
+
return this._collect(rawRes, chunks, socket);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Gather the captured status/headers/body into a TestResponse and free the socket. */
|
|
256
|
+
private _collect(rawRes: ServerResponse, chunks: Buffer[], socket: Socket): TestResponse {
|
|
189
257
|
const responseBody = Buffer.concat(chunks).toString();
|
|
190
258
|
const responseHeaders: Record<string, string> = {};
|
|
191
259
|
for (const [name, value] of Object.entries(rawRes.getHeaders())) {
|
|
@@ -193,10 +261,7 @@ export class TestClient {
|
|
|
193
261
|
responseHeaders[name] = Array.isArray(value) ? value.join(", ") : String(value);
|
|
194
262
|
}
|
|
195
263
|
}
|
|
196
|
-
|
|
197
|
-
// Clean up the socket
|
|
198
264
|
socket.destroy();
|
|
199
|
-
|
|
200
265
|
return new TestResponse(rawRes.statusCode, responseHeaders, responseBody);
|
|
201
266
|
}
|
|
202
267
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* URL format: firebird://user:pass@host:port/path/to/database.fdb
|
|
6
6
|
*/
|
|
7
7
|
import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
|
|
8
|
-
import { SQLTranslator } from "../
|
|
8
|
+
import { SQLTranslator } from "../sqlTranslator.js";
|
|
9
9
|
import { createRequire } from "node:module";
|
|
10
10
|
|
|
11
11
|
let firebird: any = null;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* URL format: mssql://user:pass@host:port/database
|
|
6
6
|
*/
|
|
7
7
|
import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
|
|
8
|
-
import { SQLTranslator } from "../
|
|
8
|
+
import { SQLTranslator } from "../sqlTranslator.js";
|
|
9
9
|
import { createRequire } from "node:module";
|
|
10
10
|
|
|
11
11
|
let tedious: any = null;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* URL format: mysql://user:pass@host:port/database
|
|
6
6
|
*/
|
|
7
7
|
import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
|
|
8
|
-
import { SQLTranslator } from "../
|
|
8
|
+
import { SQLTranslator } from "../sqlTranslator.js";
|
|
9
9
|
import { createRequire } from "node:module";
|
|
10
10
|
|
|
11
11
|
let mysql2: any = null;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* URL format: postgresql://user:pass@host:port/database
|
|
6
6
|
*/
|
|
7
7
|
import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
|
|
8
|
-
import { SQLTranslator } from "../
|
|
8
|
+
import { SQLTranslator } from "../sqlTranslator.js";
|
|
9
9
|
|
|
10
10
|
import { createRequire } from "node:module";
|
|
11
11
|
|
|
@@ -2,7 +2,7 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
2
2
|
import { mkdirSync } from "node:fs";
|
|
3
3
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
4
|
import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
|
|
5
|
-
import { SQLTranslator } from "../
|
|
5
|
+
import { SQLTranslator } from "../sqlTranslator.js";
|
|
6
6
|
|
|
7
7
|
/** A safe-to-interpolate SQL identifier (no quoting/escaping needed). */
|
|
8
8
|
function isIdentifier(str: string): boolean {
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
import { validate as validateFields } from "./validation.js";
|
|
8
8
|
import { QueryBuilder } from "./queryBuilder.js";
|
|
9
9
|
import { SQLiteAdapter } from "./adapters/sqlite.js";
|
|
10
|
-
import { QueryCache } from "./
|
|
10
|
+
import { QueryCache } from "./sqlTranslator.js";
|
|
11
11
|
import { Log } from "../../core/src/index.js";
|
|
12
12
|
import type { DatabaseAdapter, FieldDefinition, RelationshipDefinition } from "./types.js";
|
|
13
13
|
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
* db.cacheStats(); // { enabled, mode, hits, misses, size, ttl }
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
-
import { QueryCache } from "./
|
|
35
|
+
import { QueryCache } from "./sqlTranslator.js";
|
|
36
36
|
import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "./types.js";
|
|
37
37
|
import type { CacheBackend } from "../../core/src/index.js";
|
|
38
38
|
|
|
@@ -2,7 +2,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
2
2
|
import type { DatabaseAdapter, DatabaseResult as DatabaseWriteResult, ColumnInfo, FieldDefinition } from "./types.js";
|
|
3
3
|
import { DatabaseResult } from "./databaseResult.js";
|
|
4
4
|
import { CachedDatabaseAdapter, type CachedAdapterOptions } from "./cachedDatabase.js";
|
|
5
|
-
import { QueryCache } from "./
|
|
5
|
+
import { QueryCache } from "./sqlTranslator.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* v3.13.12 — strip trailing `;` and whitespace from user-supplied SQL
|
|
@@ -53,7 +53,7 @@ export { validate } from "./validation.js";
|
|
|
53
53
|
export type { ValidationError } from "./validation.js";
|
|
54
54
|
export { BaseModel, snakeToCamel, camelToSnake } from "./baseModel.js";
|
|
55
55
|
export { QueryBuilder } from "./queryBuilder.js";
|
|
56
|
-
export { SQLTranslator, QueryCache } from "./
|
|
56
|
+
export { SQLTranslator, QueryCache } from "./sqlTranslator.js";
|
|
57
57
|
export { CachedDatabaseAdapter } from "./cachedDatabase.js";
|
|
58
58
|
export type { CachedAdapterOptions } from "./cachedDatabase.js";
|
|
59
59
|
export { FakeData } from "./fakeData.js";
|
|
File without changes
|