tina4-nodejs 3.10.90 → 3.10.92
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 +1 -1
- package/packages/core/src/ai.ts +1 -1
- package/packages/core/src/api.ts +6 -6
- package/packages/core/src/auth.ts +28 -15
- package/packages/core/src/cache.ts +9 -0
- package/packages/core/src/devAdmin.ts +85 -7
- package/packages/core/src/devMailbox.ts +21 -21
- package/packages/core/src/fakeData.ts +24 -14
- package/packages/core/src/graphql.ts +37 -1
- package/packages/core/src/i18n.ts +1 -1
- package/packages/core/src/index.ts +6 -6
- package/packages/core/src/mcp.ts +3 -0
- package/packages/core/src/messenger.ts +52 -4
- package/packages/core/src/middleware.ts +61 -0
- package/packages/core/src/queue.ts +103 -30
- package/packages/core/src/queueBackends/liteBackend.ts +43 -0
- package/packages/core/src/rateLimiter.ts +88 -1
- package/packages/core/src/request.ts +24 -1
- package/packages/core/src/response.ts +54 -10
- package/packages/core/src/router.ts +32 -14
- package/packages/core/src/scss.ts +44 -2
- package/packages/core/src/server.ts +26 -4
- package/packages/core/src/service.ts +7 -0
- package/packages/core/src/session.ts +4 -4
- package/packages/core/src/testClient.ts +2 -2
- package/packages/core/src/testing.ts +6 -6
- package/packages/core/src/types.ts +8 -1
- package/packages/core/src/watcher.ts +66 -0
- package/packages/core/src/websocket.ts +24 -3
- package/packages/core/src/websocketConnection.ts +4 -0
- package/packages/core/src/wsdl.ts +12 -12
- package/packages/frond/src/engine.ts +6 -0
- package/packages/orm/src/adapters/firebird.ts +2 -2
- package/packages/orm/src/adapters/mssql.ts +2 -2
- package/packages/orm/src/adapters/mysql.ts +2 -2
- package/packages/orm/src/adapters/postgres.ts +2 -2
- package/packages/orm/src/adapters/sqlite.ts +3 -3
- package/packages/orm/src/autoCrud.ts +117 -74
- package/packages/orm/src/baseModel.ts +44 -7
- package/packages/orm/src/database.ts +58 -15
- package/packages/orm/src/databaseResult.ts +5 -0
- package/packages/orm/src/fakeData.ts +1 -11
- package/packages/orm/src/index.ts +1 -1
- package/packages/orm/src/migration.ts +78 -5
- package/packages/orm/src/queryBuilder.ts +2 -2
- package/packages/orm/src/sqlTranslation.ts +20 -3
- package/packages/orm/src/types.ts +2 -2
- package/packages/swagger/src/generator.ts +2 -2
- package/packages/swagger/src/index.ts +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Tina4 SCSS — Zero-dependency SCSS-to-CSS compiler (subset).
|
|
2
2
|
// Supports variables, nesting, & parent selector, @import, @mixin/@include, comments, basic math.
|
|
3
3
|
|
|
4
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
5
|
-
import { join, resolve, dirname } from "node:path";
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
5
|
+
import { join, resolve, dirname, basename } from "node:path";
|
|
6
6
|
|
|
7
7
|
// ── Types ────────────────────────────────────────────────────────
|
|
8
8
|
|
|
@@ -46,6 +46,48 @@ export class ScssCompiler {
|
|
|
46
46
|
const key = name.startsWith("$") ? name.slice(1) : name;
|
|
47
47
|
this._variables[key] = value;
|
|
48
48
|
}
|
|
49
|
+
|
|
50
|
+
/** Compile all .scss files in a directory into a single CSS output file. */
|
|
51
|
+
compileScss(scssDir: string = "src/scss", output: string = "public/css/default.css", minify: boolean = false): string {
|
|
52
|
+
const absDir = resolve(scssDir);
|
|
53
|
+
if (!existsSync(absDir)) return "";
|
|
54
|
+
|
|
55
|
+
// Collect non-partial .scss files, sorted
|
|
56
|
+
const files = readdirSync(absDir)
|
|
57
|
+
.filter((f) => f.endsWith(".scss") && !f.startsWith("_"))
|
|
58
|
+
.sort()
|
|
59
|
+
.map((f) => join(absDir, f));
|
|
60
|
+
|
|
61
|
+
if (files.length === 0) return "";
|
|
62
|
+
|
|
63
|
+
// Merge all files, resolving imports
|
|
64
|
+
const paths = [absDir, ...this._importPaths];
|
|
65
|
+
const imported = new Set<string>();
|
|
66
|
+
let merged = "";
|
|
67
|
+
for (const file of files) {
|
|
68
|
+
const content = readFileSync(file, "utf-8");
|
|
69
|
+
imported.add(file);
|
|
70
|
+
merged += resolveImports(content, paths, imported) + "\n";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let css = compileString(merged, paths, { ...this._variables });
|
|
74
|
+
|
|
75
|
+
if (minify) {
|
|
76
|
+
css = css.replace(/\/\*.*?\*\//gs, "");
|
|
77
|
+
css = css.replace(/\s+/g, " ");
|
|
78
|
+
css = css.replace(/\s*([{}:;,])\s*/g, "$1");
|
|
79
|
+
css = css.replace(/;}/g, "}");
|
|
80
|
+
css = css.trim();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Write output
|
|
84
|
+
const absOutput = resolve(output);
|
|
85
|
+
const outDir = dirname(absOutput);
|
|
86
|
+
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
|
|
87
|
+
writeFileSync(absOutput, css, "utf-8");
|
|
88
|
+
|
|
89
|
+
return css;
|
|
90
|
+
}
|
|
49
91
|
}
|
|
50
92
|
|
|
51
93
|
// ── Internal Compilation Pipeline ────────────────────────────────
|
|
@@ -10,7 +10,7 @@ import type { Tina4Config, Tina4Request, Tina4Response } from "./types.js";
|
|
|
10
10
|
import { Router, defaultRouter, runRouteMiddlewares } from "./router.js";
|
|
11
11
|
import { validToken, getPayload } from "./auth.js";
|
|
12
12
|
import { discoverRoutes } from "./routeDiscovery.js";
|
|
13
|
-
import { createRequest
|
|
13
|
+
import { createRequest } from "./request.js";
|
|
14
14
|
import { createResponse, setDefaultTemplatesDir } from "./response.js";
|
|
15
15
|
import { MiddlewareChain, cors, requestLogger } from "./middleware.js";
|
|
16
16
|
import { tryServeStatic } from "./static.js";
|
|
@@ -439,6 +439,28 @@ function deployGallery(name) {
|
|
|
439
439
|
// Allows handle() to route requests without requiring a reference to the server.
|
|
440
440
|
let _dispatchFn: ((rawReq: IncomingMessage, rawRes: ServerResponse) => Promise<void>) | null = null;
|
|
441
441
|
|
|
442
|
+
/** Module-level server handle for start()/stop() parity. */
|
|
443
|
+
let _serverHandle: { close: () => void; router: Router; port: number } | null = null;
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Start the Tina4 HTTP server.
|
|
447
|
+
* Thin wrapper around startServer() for cross-framework parity with PHP and Ruby.
|
|
448
|
+
*/
|
|
449
|
+
export async function start(config?: Tina4Config): Promise<{ close: () => void; router: Router; port: number }> {
|
|
450
|
+
_serverHandle = await startServer(config);
|
|
451
|
+
return _serverHandle;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Stop the running Tina4 server gracefully.
|
|
456
|
+
*/
|
|
457
|
+
export function stop(): void {
|
|
458
|
+
if (_serverHandle) {
|
|
459
|
+
_serverHandle.close();
|
|
460
|
+
_serverHandle = null;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
442
464
|
/**
|
|
443
465
|
* Dispatch a raw Node.js request through the Tina4 router and write the response.
|
|
444
466
|
* Requires startServer() to have been called first.
|
|
@@ -640,7 +662,7 @@ ${reset}
|
|
|
640
662
|
// ORM not available, swagger will work without model schemas
|
|
641
663
|
}
|
|
642
664
|
|
|
643
|
-
const getSpec = () => swagger.
|
|
665
|
+
const getSpec = () => swagger.generate(allRoutes, modelDefs as any);
|
|
644
666
|
const swaggerRoutes = swagger.createSwaggerRoutes(getSpec);
|
|
645
667
|
for (const route of swaggerRoutes) {
|
|
646
668
|
router.addRoute(route);
|
|
@@ -696,7 +718,7 @@ ${reset}
|
|
|
696
718
|
if (res.raw.writableEnded) return;
|
|
697
719
|
|
|
698
720
|
// Parse request body
|
|
699
|
-
await parseBody(
|
|
721
|
+
await req.parseBody();
|
|
700
722
|
|
|
701
723
|
const pathname = (req.url ?? "/").split("?")[0];
|
|
702
724
|
|
|
@@ -828,7 +850,7 @@ ${reset}
|
|
|
828
850
|
typeof result === "object" &&
|
|
829
851
|
!Buffer.isBuffer(result)
|
|
830
852
|
) {
|
|
831
|
-
await res.
|
|
853
|
+
await res.render(match.template, result as Record<string, unknown>);
|
|
832
854
|
}
|
|
833
855
|
|
|
834
856
|
if (!res.raw.writableEnded) {
|
|
@@ -322,6 +322,13 @@ export class ServiceRunner {
|
|
|
322
322
|
registry.clear();
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
+
/**
|
|
326
|
+
* Check if a 5-field cron pattern matches the given (or current) date/time.
|
|
327
|
+
*/
|
|
328
|
+
static matchCron(pattern: string, now?: Date): boolean {
|
|
329
|
+
return matchesCron(pattern, now ?? new Date());
|
|
330
|
+
}
|
|
331
|
+
|
|
325
332
|
/**
|
|
326
333
|
* Watch service files for changes and hot-reload in dev mode.
|
|
327
334
|
*/
|
|
@@ -68,7 +68,7 @@ interface SessionData {
|
|
|
68
68
|
*/
|
|
69
69
|
export interface SessionHandler {
|
|
70
70
|
read(sessionId: string): SessionData | null;
|
|
71
|
-
write(sessionId: string, data: SessionData, ttl
|
|
71
|
+
write(sessionId: string, data: SessionData, ttl?: number): void;
|
|
72
72
|
destroy(sessionId: string): void;
|
|
73
73
|
/** Garbage-collect expired sessions. Optional — Redis/Valkey/Mongo handle TTL natively. */
|
|
74
74
|
gc?(maxLifetime: number): void;
|
|
@@ -112,7 +112,7 @@ export class FileSessionHandler implements SessionHandler {
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
write(sessionId: string, data: SessionData, ttl: number): void {
|
|
115
|
+
write(sessionId: string, data: SessionData, ttl: number = 0): void {
|
|
116
116
|
this.ensureDir();
|
|
117
117
|
const expires = ttl > 0 ? Math.floor(Date.now() / 1000) + ttl : 0;
|
|
118
118
|
const wrapper = { _data: data, _expires: expires };
|
|
@@ -126,7 +126,7 @@ export class FileSessionHandler implements SessionHandler {
|
|
|
126
126
|
} catch { /* ignore */ }
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
gc(
|
|
129
|
+
gc(maxLifetime: number = 0): void {
|
|
130
130
|
if (!existsSync(this.storagePath)) return;
|
|
131
131
|
const now = Math.floor(Date.now() / 1000);
|
|
132
132
|
try {
|
|
@@ -298,7 +298,7 @@ export class RedisSessionHandler implements SessionHandler {
|
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
-
write(sessionId: string, data: SessionData, ttl: number): void {
|
|
301
|
+
write(sessionId: string, data: SessionData, ttl: number = 0): void {
|
|
302
302
|
const json = JSON.stringify(data);
|
|
303
303
|
if (ttl > 0) {
|
|
304
304
|
this.execSync(["SETEX", this.key(sessionId), String(ttl), json]);
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { IncomingMessage, ServerResponse } from "node:http";
|
|
18
18
|
import { Socket } from "node:net";
|
|
19
|
-
import { createRequest
|
|
19
|
+
import { createRequest } from "./request.js";
|
|
20
20
|
import { createResponse } from "./response.js";
|
|
21
21
|
import { defaultRouter, type Router } from "./router.js";
|
|
22
22
|
|
|
@@ -153,7 +153,7 @@ export class TestClient {
|
|
|
153
153
|
const res = createResponse(rawRes);
|
|
154
154
|
|
|
155
155
|
// Parse body (populates req.body)
|
|
156
|
-
await parseBody(
|
|
156
|
+
await req.parseBody();
|
|
157
157
|
|
|
158
158
|
// Split path for route matching
|
|
159
159
|
const cleanPath = path.includes("?") ? path.split("?")[0] : path;
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Attach test assertions to functions and run them all at once.
|
|
5
5
|
*
|
|
6
|
-
* import { tests, assertEqual,
|
|
6
|
+
* import { tests, assertEqual, assertRaises, runAll } from "./testing.js";
|
|
7
7
|
*
|
|
8
8
|
* const add = tests(
|
|
9
9
|
* assertEqual([5, 3], 8),
|
|
10
|
-
*
|
|
10
|
+
* assertRaises(Error, [null]),
|
|
11
11
|
* )(function add(a: number, b: number | null = null): number {
|
|
12
12
|
* if (b === null) throw new Error("b required");
|
|
13
13
|
* return a + b;
|
|
14
14
|
* });
|
|
15
15
|
*
|
|
16
|
-
*
|
|
16
|
+
* runAll();
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
// ── Types ──────────────────────────────────────────────────────────
|
|
@@ -50,7 +50,7 @@ export function assertEqual(args: unknown[], expected: unknown): Assertion {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/** Assert that calling the function with `args` throws an instance of `errorClass`. */
|
|
53
|
-
export function
|
|
53
|
+
export function assertRaises(
|
|
54
54
|
errorClass: new (...a: unknown[]) => Error,
|
|
55
55
|
args: unknown[],
|
|
56
56
|
): Assertion {
|
|
@@ -93,7 +93,7 @@ export function tests(
|
|
|
93
93
|
// ── Runner ─────────────────────────────────────────────────────────
|
|
94
94
|
|
|
95
95
|
/** Run all registered tests. Returns results summary. */
|
|
96
|
-
export function
|
|
96
|
+
export function runAll(
|
|
97
97
|
options: { quiet?: boolean; failfast?: boolean } = {},
|
|
98
98
|
): TestResults {
|
|
99
99
|
const { quiet = false, failfast = false } = options;
|
|
@@ -141,7 +141,7 @@ export function runAllTests(
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
/** Reset the test registry (useful between test runs). */
|
|
144
|
-
export function
|
|
144
|
+
export function reset(): void {
|
|
145
145
|
registry.length = 0;
|
|
146
146
|
}
|
|
147
147
|
|
|
@@ -27,6 +27,14 @@ export interface Tina4Request extends IncomingMessage {
|
|
|
27
27
|
contentType: string;
|
|
28
28
|
session: Tina4Session;
|
|
29
29
|
user?: Record<string, unknown>;
|
|
30
|
+
/** Get a specific header value by name (case-insensitive). */
|
|
31
|
+
header(name: string): string | undefined;
|
|
32
|
+
/** Extract the Bearer token from the Authorization header. */
|
|
33
|
+
bearerToken(): string | null;
|
|
34
|
+
/** Get a parameter by key from merged params (route + query). */
|
|
35
|
+
param(key: string, defaultValue?: string): string | undefined;
|
|
36
|
+
/** Parse the request body based on content type. */
|
|
37
|
+
parseBody(): Promise<void>;
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
export interface CookieOptions {
|
|
@@ -53,7 +61,6 @@ export interface Tina4ResponseMethods {
|
|
|
53
61
|
file(path: string, options?: { download?: boolean; contentType?: string }): Tina4Response;
|
|
54
62
|
error(code: string, message: string, status?: number): Tina4Response;
|
|
55
63
|
render(template: string, data?: Record<string, unknown>, status?: number, templateDir?: string): Promise<Tina4Response>;
|
|
56
|
-
template(name: string, data?: Record<string, unknown>, status?: number, templateDir?: string): Promise<Tina4Response>;
|
|
57
64
|
/** Stream response from an async generator (SSE or chunked). */
|
|
58
65
|
stream(source: AsyncIterable<string | Buffer>, contentType?: string): Promise<Tina4Response>;
|
|
59
66
|
/** The underlying ServerResponse for advanced use */
|
|
@@ -7,6 +7,72 @@ import { resolve, extname } from "node:path";
|
|
|
7
7
|
*/
|
|
8
8
|
const CODE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
9
9
|
|
|
10
|
+
// Module-level state for start()/stop() API
|
|
11
|
+
let _watchers: ReturnType<typeof watch>[] = [];
|
|
12
|
+
let _debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
13
|
+
let _codeChangePending = false;
|
|
14
|
+
let _running = false;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Start the DevReload file watcher.
|
|
18
|
+
*
|
|
19
|
+
* Watches the given directories for file changes and calls onChange when
|
|
20
|
+
* a change is detected. Mirrors the Python DevReload.start() API.
|
|
21
|
+
*
|
|
22
|
+
* @param dirs - Directories to watch. Defaults to ["src", "public"].
|
|
23
|
+
* @param onChange - Callback invoked on file change. Receives `{ code: boolean }`.
|
|
24
|
+
*/
|
|
25
|
+
export function start(
|
|
26
|
+
dirs: string[] = ["src", "public"],
|
|
27
|
+
onChange: (info: { code: boolean }) => void = () => {},
|
|
28
|
+
): void {
|
|
29
|
+
if (_running) return;
|
|
30
|
+
_running = true;
|
|
31
|
+
|
|
32
|
+
const debouncedOnChange = () => {
|
|
33
|
+
if (_debounceTimer) clearTimeout(_debounceTimer);
|
|
34
|
+
_debounceTimer = setTimeout(() => {
|
|
35
|
+
const code = _codeChangePending;
|
|
36
|
+
_codeChangePending = false;
|
|
37
|
+
console.log(
|
|
38
|
+
`\n \x1b[33mFile change detected${code ? ", reloading routes" : ""}...\x1b[0m\n`,
|
|
39
|
+
);
|
|
40
|
+
onChange({ code });
|
|
41
|
+
}, 200);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
for (const dir of dirs) {
|
|
45
|
+
if (!existsSync(dir)) continue;
|
|
46
|
+
try {
|
|
47
|
+
const watcher = watch(resolve(dir), { recursive: true }, (_event, filename) => {
|
|
48
|
+
if (filename && CODE_EXTENSIONS.has(extname(filename))) {
|
|
49
|
+
_codeChangePending = true;
|
|
50
|
+
}
|
|
51
|
+
debouncedOnChange();
|
|
52
|
+
});
|
|
53
|
+
_watchers.push(watcher);
|
|
54
|
+
} catch {
|
|
55
|
+
console.warn(` Warning: Could not watch ${dir}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Stop the DevReload file watcher.
|
|
62
|
+
*
|
|
63
|
+
* Closes all active file watchers and resets internal state.
|
|
64
|
+
* Mirrors the Python DevReload.stop() API.
|
|
65
|
+
*/
|
|
66
|
+
export function stop(): void {
|
|
67
|
+
if (!_running) return;
|
|
68
|
+
_running = false;
|
|
69
|
+
for (const w of _watchers) w.close();
|
|
70
|
+
_watchers = [];
|
|
71
|
+
if (_debounceTimer) clearTimeout(_debounceTimer);
|
|
72
|
+
_debounceTimer = null;
|
|
73
|
+
_codeChangePending = false;
|
|
74
|
+
}
|
|
75
|
+
|
|
10
76
|
/**
|
|
11
77
|
* Watch directories for file changes.
|
|
12
78
|
*
|
|
@@ -58,7 +58,7 @@ export interface WebSocketClient {
|
|
|
58
58
|
|
|
59
59
|
type EventHandler = (...args: unknown[]) => void;
|
|
60
60
|
|
|
61
|
-
// ── Frame Utilities (
|
|
61
|
+
// ── Frame Utilities (internal) ───────────────────────────────
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
64
|
* Compute Sec-WebSocket-Accept from Sec-WebSocket-Key per RFC 6455.
|
|
@@ -214,9 +214,9 @@ export class WebSocketServer {
|
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
/**
|
|
217
|
-
* Send a message to a specific client.
|
|
217
|
+
* Send a message to a specific client by ID.
|
|
218
218
|
*/
|
|
219
|
-
|
|
219
|
+
sendTo(clientId: string, message: string): void {
|
|
220
220
|
const client = this.clients.get(clientId);
|
|
221
221
|
if (!client || client.closed) return;
|
|
222
222
|
|
|
@@ -285,6 +285,27 @@ export class WebSocketServer {
|
|
|
285
285
|
return this.clients;
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Close a specific client connection with an optional code and reason.
|
|
290
|
+
*/
|
|
291
|
+
close(clientId: string, code: number = 1000, reason: string = ""): void {
|
|
292
|
+
const client = this.clients.get(clientId);
|
|
293
|
+
if (!client || client.closed) return;
|
|
294
|
+
client.closed = true;
|
|
295
|
+
const reasonBytes = Buffer.from(reason, "utf-8");
|
|
296
|
+
const payload = Buffer.alloc(2 + reasonBytes.length);
|
|
297
|
+
payload.writeUInt16BE(code, 0);
|
|
298
|
+
reasonBytes.copy(payload, 2);
|
|
299
|
+
try {
|
|
300
|
+
client.socket.write(buildFrame(OP_CLOSE, payload));
|
|
301
|
+
client.socket.end();
|
|
302
|
+
} catch {
|
|
303
|
+
// already closed
|
|
304
|
+
}
|
|
305
|
+
this.clients.delete(clientId);
|
|
306
|
+
this.removeClientFromAllRooms(clientId);
|
|
307
|
+
}
|
|
308
|
+
|
|
288
309
|
// ── Rooms ──────────────────────────────────────────────────
|
|
289
310
|
|
|
290
311
|
/**
|
|
@@ -17,6 +17,10 @@ export interface WebSocketConnection {
|
|
|
17
17
|
send(message: string): void;
|
|
18
18
|
/** Broadcast a message to all connections on the same path (path-scoped) */
|
|
19
19
|
broadcast(message: string): void;
|
|
20
|
+
/** Join a room */
|
|
21
|
+
joinRoom(roomName: string): void;
|
|
22
|
+
/** Leave a room */
|
|
23
|
+
leaveRoom(roomName: string): void;
|
|
20
24
|
/** Close this connection */
|
|
21
25
|
close(): void;
|
|
22
26
|
}
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Matches the PHP reference implementation (Tina4\WSDL).
|
|
8
8
|
*
|
|
9
|
-
* import { WSDLService,
|
|
9
|
+
* import { WSDLService, WSDLOperation } from "@tina4/core";
|
|
10
10
|
*
|
|
11
11
|
* class Calculator extends WSDLService {
|
|
12
12
|
* serviceName = "Calculator";
|
|
13
13
|
* serviceUrl = "/api/calculator";
|
|
14
14
|
*
|
|
15
|
-
* @
|
|
15
|
+
* @WSDLOperation({ output: { Result: "int" } })
|
|
16
16
|
* async Add(a: number, b: number): Promise<Record<string, unknown>> {
|
|
17
17
|
* return { Result: a + b };
|
|
18
18
|
* }
|
|
@@ -21,14 +21,14 @@
|
|
|
21
21
|
|
|
22
22
|
// ── Types ────────────────────────────────────────────────────
|
|
23
23
|
|
|
24
|
-
export interface
|
|
24
|
+
export interface WSDLOperationMeta {
|
|
25
25
|
name: string;
|
|
26
26
|
description?: string;
|
|
27
27
|
input?: Record<string, string>; // param name -> type
|
|
28
28
|
output?: Record<string, string>; // return name -> type
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
interface
|
|
31
|
+
interface WSDLOperationConfig {
|
|
32
32
|
description?: string;
|
|
33
33
|
input?: Record<string, string>;
|
|
34
34
|
output?: Record<string, string>;
|
|
@@ -167,13 +167,13 @@ const WSDL_OPS_KEY = Symbol("wsdl_operations");
|
|
|
167
167
|
/**
|
|
168
168
|
* Decorator function for marking methods as WSDL operations.
|
|
169
169
|
*
|
|
170
|
-
* @
|
|
170
|
+
* @WSDLOperation({ description: "Add two numbers", input: { a: "int", b: "int" }, output: { Result: "int" } })
|
|
171
171
|
* async Add(a: number, b: number): Promise<Record<string, unknown>> { ... }
|
|
172
172
|
*/
|
|
173
|
-
export function
|
|
173
|
+
export function WSDLOperation(config?: WSDLOperationConfig) {
|
|
174
174
|
return function (_target: unknown, propertyKey: string, descriptor: PropertyDescriptor) {
|
|
175
175
|
// Store metadata on the method itself
|
|
176
|
-
const op:
|
|
176
|
+
const op: WSDLOperationMeta = {
|
|
177
177
|
name: propertyKey,
|
|
178
178
|
description: config?.description,
|
|
179
179
|
input: config?.input,
|
|
@@ -214,12 +214,12 @@ export abstract class WSDLService {
|
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
/** Discovered operations (populated on first use). */
|
|
217
|
-
private _operations: Map<string,
|
|
217
|
+
private _operations: Map<string, WSDLOperationMeta> | null = null;
|
|
218
218
|
|
|
219
219
|
/**
|
|
220
220
|
* Discover operations by scanning for methods with _wsdlOp metadata.
|
|
221
221
|
*/
|
|
222
|
-
private discoverOperations(): Map<string,
|
|
222
|
+
private discoverOperations(): Map<string, WSDLOperationMeta> {
|
|
223
223
|
if (this._operations) return this._operations;
|
|
224
224
|
|
|
225
225
|
this._operations = new Map();
|
|
@@ -233,7 +233,7 @@ export abstract class WSDLService {
|
|
|
233
233
|
try {
|
|
234
234
|
const method = (this as Record<string, unknown>)[name];
|
|
235
235
|
if (typeof method === "function" && (method as unknown as Record<string, unknown>)._wsdlOp) {
|
|
236
|
-
const op = (method as unknown as Record<string, unknown>)._wsdlOp as
|
|
236
|
+
const op = (method as unknown as Record<string, unknown>)._wsdlOp as WSDLOperationMeta;
|
|
237
237
|
if (!this._operations.has(name)) {
|
|
238
238
|
this._operations.set(name, op);
|
|
239
239
|
}
|
|
@@ -386,7 +386,7 @@ export abstract class WSDLService {
|
|
|
386
386
|
/**
|
|
387
387
|
* Handle incoming SOAP request (parse XML, dispatch to method, return SOAP response).
|
|
388
388
|
*/
|
|
389
|
-
async
|
|
389
|
+
async handle(soapXml: string = ""): Promise<string> {
|
|
390
390
|
const ops = this.discoverOperations();
|
|
391
391
|
|
|
392
392
|
// Parse SOAP body
|
|
@@ -538,7 +538,7 @@ export abstract class WSDLService {
|
|
|
538
538
|
return;
|
|
539
539
|
}
|
|
540
540
|
|
|
541
|
-
const soapResponse = await this.
|
|
541
|
+
const soapResponse = await this.handle(xmlBody);
|
|
542
542
|
|
|
543
543
|
if (typeof res.send === "function") {
|
|
544
544
|
if (typeof res.setHeader === "function") {
|
|
@@ -1370,6 +1370,12 @@ export class Frond {
|
|
|
1370
1370
|
this.compiledStrings.clear();
|
|
1371
1371
|
}
|
|
1372
1372
|
|
|
1373
|
+
/** Render a debug dump of a value as HTML — parity with PHP/Ruby/Python.
|
|
1374
|
+
* Gated on TINA4_DEBUG=true. Returns empty string in production. */
|
|
1375
|
+
renderDump(value: unknown): string {
|
|
1376
|
+
return renderDump(value).toString();
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1373
1379
|
private load(name: string): string {
|
|
1374
1380
|
const filePath = join(this.templateDir, name);
|
|
1375
1381
|
if (!existsSync(filePath)) {
|
|
@@ -212,7 +212,7 @@ export class FirebirdAdapter implements DatabaseAdapter {
|
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
update(table: string, data: Record<string, unknown>, filter: Record<string, unknown
|
|
215
|
+
update(table: string, data: Record<string, unknown>, filter: Record<string, unknown>, params?: unknown[]): DatabaseResult {
|
|
216
216
|
throw new Error("Use updateAsync() for Firebird.");
|
|
217
217
|
}
|
|
218
218
|
|
|
@@ -231,7 +231,7 @@ export class FirebirdAdapter implements DatabaseAdapter {
|
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
delete(table: string, filter: Record<string, unknown
|
|
234
|
+
delete(table: string, filter: Record<string, unknown>, params?: unknown[]): DatabaseResult {
|
|
235
235
|
throw new Error("Use deleteAsync() for Firebird.");
|
|
236
236
|
}
|
|
237
237
|
|
|
@@ -257,7 +257,7 @@ export class MssqlAdapter implements DatabaseAdapter {
|
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
update(table: string, data: Record<string, unknown>, filter: Record<string, unknown
|
|
260
|
+
update(table: string, data: Record<string, unknown>, filter: Record<string, unknown>, params?: unknown[]): DatabaseResult {
|
|
261
261
|
throw new Error("Use updateAsync() for MSSQL.");
|
|
262
262
|
}
|
|
263
263
|
|
|
@@ -280,7 +280,7 @@ export class MssqlAdapter implements DatabaseAdapter {
|
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
delete(table: string, filter: Record<string, unknown
|
|
283
|
+
delete(table: string, filter: Record<string, unknown>, params?: unknown[]): DatabaseResult {
|
|
284
284
|
throw new Error("Use deleteAsync() for MSSQL.");
|
|
285
285
|
}
|
|
286
286
|
|
|
@@ -183,7 +183,7 @@ export class MysqlAdapter implements DatabaseAdapter {
|
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
update(table: string, data: Record<string, unknown>, filter: Record<string, unknown
|
|
186
|
+
update(table: string, data: Record<string, unknown>, filter: Record<string, unknown>, params?: unknown[]): DatabaseResult {
|
|
187
187
|
throw new Error("Use updateAsync() for MySQL.");
|
|
188
188
|
}
|
|
189
189
|
|
|
@@ -202,7 +202,7 @@ export class MysqlAdapter implements DatabaseAdapter {
|
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
delete(table: string, filter: Record<string, unknown
|
|
205
|
+
delete(table: string, filter: Record<string, unknown>, params?: unknown[]): DatabaseResult {
|
|
206
206
|
throw new Error("Use deleteAsync() for MySQL.");
|
|
207
207
|
}
|
|
208
208
|
|
|
@@ -167,7 +167,7 @@ export class PostgresAdapter implements DatabaseAdapter {
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
update(table: string, data: Record<string, unknown>, filter: Record<string, unknown
|
|
170
|
+
update(table: string, data: Record<string, unknown>, filter: Record<string, unknown>, params?: unknown[]): DatabaseResult {
|
|
171
171
|
throw new Error("Use updateAsync() for PostgreSQL.");
|
|
172
172
|
}
|
|
173
173
|
|
|
@@ -190,7 +190,7 @@ export class PostgresAdapter implements DatabaseAdapter {
|
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
delete(table: string, filter: Record<string, unknown
|
|
193
|
+
delete(table: string, filter: Record<string, unknown>, params?: unknown[]): DatabaseResult {
|
|
194
194
|
throw new Error("Use deleteAsync() for PostgreSQL.");
|
|
195
195
|
}
|
|
196
196
|
|
|
@@ -100,7 +100,7 @@ export class SQLiteAdapter implements DatabaseAdapter {
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
update(table: string, data: Record<string, unknown>, filter: Record<string, unknown
|
|
103
|
+
update(table: string, data: Record<string, unknown>, filter: Record<string, unknown>, params?: unknown[]): DatabaseResult {
|
|
104
104
|
const setClauses = Object.keys(data).map((k) => `"${k}" = ?`).join(", ");
|
|
105
105
|
const whereClauses = Object.keys(filter).map((k) => `"${k}" = ?`).join(" AND ");
|
|
106
106
|
const sql = `UPDATE "${table}" SET ${setClauses} WHERE ${whereClauses}`;
|
|
@@ -114,7 +114,7 @@ export class SQLiteAdapter implements DatabaseAdapter {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
delete(table: string, filter: Record<string, unknown> | string | Record<string, unknown>[]): DatabaseResult {
|
|
117
|
+
delete(table: string, filter: Record<string, unknown> | string | Record<string, unknown>[], params?: unknown[]): DatabaseResult {
|
|
118
118
|
if (Array.isArray(filter)) {
|
|
119
119
|
let totalAffected = 0;
|
|
120
120
|
for (const row of filter) {
|
|
@@ -127,7 +127,7 @@ export class SQLiteAdapter implements DatabaseAdapter {
|
|
|
127
127
|
if (typeof filter === "string") {
|
|
128
128
|
const sql = filter ? `DELETE FROM "${table}" WHERE ${filter}` : `DELETE FROM "${table}"`;
|
|
129
129
|
try {
|
|
130
|
-
const result = this.db.prepare(sql).run();
|
|
130
|
+
const result = this.db.prepare(sql).run(...(params ?? []));
|
|
131
131
|
return { success: true, rowsAffected: result.changes };
|
|
132
132
|
} catch (e) {
|
|
133
133
|
return { success: false, rowsAffected: 0, error: (e as Error).message };
|