tina4-nodejs 3.13.79 → 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.
@@ -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
- let resolved = value.trim();
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, type Router } from "./router.js";
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 match = this.router.match(method.toUpperCase(), cleanPath);
164
+ const httpMethod = method.toUpperCase();
165
+ const match = this.router.match(httpMethod, cleanPath);
164
166
  if (!match) {
165
- return new TestResponse(404, { "content-type": "application/json" }, '{"error":"Not found"}');
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
- // Collect response
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 "../sqlTranslation.js";
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 "../sqlTranslation.js";
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 "../sqlTranslation.js";
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 "../sqlTranslation.js";
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 "../sqlTranslation.js";
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 "./sqlTranslation.js";
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 "./sqlTranslation.js";
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 "./sqlTranslation.js";
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 "./sqlTranslation.js";
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";