tina4-nodejs 3.13.81 → 3.13.82
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/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);
|
|
@@ -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
|