tina4-nodejs 3.8.1 → 3.8.3
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/index.ts +4 -2
- package/packages/core/src/middleware.ts +53 -0
- package/packages/core/src/request.ts +22 -0
- package/packages/core/src/response.ts +15 -0
- package/packages/core/src/types.ts +1 -0
- package/packages/core/src/validator.ts +145 -0
- package/packages/orm/src/database.ts +96 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tina4-nodejs",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "This is not a framework. Tina4 for Node.js/TypeScript — zero deps, 38 built-in features.",
|
|
6
6
|
"keywords": ["tina4", "framework", "web", "api", "orm", "graphql", "websocket", "typescript"],
|
|
@@ -17,10 +17,10 @@ export { Router, RouteGroup, RouteRef, defaultRouter, runRouteMiddlewares } from
|
|
|
17
17
|
export { get, post, put, patch, del, any, websocket, del as delete } from "./router.js";
|
|
18
18
|
export type { RouteInfo } from "./router.js";
|
|
19
19
|
export { discoverRoutes } from "./routeDiscovery.js";
|
|
20
|
-
export { MiddlewareChain, MiddlewareRunner, cors, requestLogger, CorsMiddleware, RateLimiterMiddleware, RequestLogger } from "./middleware.js";
|
|
20
|
+
export { MiddlewareChain, MiddlewareRunner, cors, requestLogger, CorsMiddleware, RateLimiterMiddleware, RequestLogger, SecurityHeadersMiddleware } from "./middleware.js";
|
|
21
21
|
export type { CorsConfig } from "./middleware.js";
|
|
22
22
|
export { createRequest, parseBody } from "./request.js";
|
|
23
|
-
export { createResponse } from "./response.js";
|
|
23
|
+
export { createResponse, errorResponse } from "./response.js";
|
|
24
24
|
export { tryServeStatic } from "./static.js";
|
|
25
25
|
export { loadEnv, getEnv, requireEnv, hasEnv, allEnv, resetEnv, isTruthy } from "./dotenv.js";
|
|
26
26
|
export { Log } from "./logger.js";
|
|
@@ -95,4 +95,6 @@ export { RedisNpmSessionHandler } from "./sessionHandlers/redisHandler.js";
|
|
|
95
95
|
export type { RedisNpmSessionConfig } from "./sessionHandlers/redisHandler.js";
|
|
96
96
|
export { tests, assertEqual, assertThrows, assertTrue, assertFalse, runAllTests, resetTests } from "./testing.js";
|
|
97
97
|
export { Container, container } from "./container.js";
|
|
98
|
+
export { Validator } from "./validator.js";
|
|
99
|
+
export type { ValidationError } from "./validator.js";
|
|
98
100
|
export type { WebSocketConnection } from "./websocketConnection.js";
|
|
@@ -349,6 +349,59 @@ export class RequestLogger {
|
|
|
349
349
|
}
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Class-based security headers middleware using the before/after convention.
|
|
354
|
+
* Auto-injects security headers on every response.
|
|
355
|
+
*
|
|
356
|
+
* Configuration via env vars:
|
|
357
|
+
* TINA4_FRAME_OPTIONS — X-Frame-Options (default: "SAMEORIGIN")
|
|
358
|
+
* TINA4_HSTS — Strict-Transport-Security max-age value
|
|
359
|
+
* (default: "" = off; set to "31536000" to enable)
|
|
360
|
+
* TINA4_CSP — Content-Security-Policy (default: "default-src 'self'")
|
|
361
|
+
* TINA4_REFERRER_POLICY — Referrer-Policy (default: "strict-origin-when-cross-origin")
|
|
362
|
+
* TINA4_PERMISSIONS_POLICY — Permissions-Policy (default: "camera=(), microphone=(), geolocation=()")
|
|
363
|
+
*
|
|
364
|
+
* Usage:
|
|
365
|
+
* Router.use(SecurityHeadersMiddleware);
|
|
366
|
+
*/
|
|
367
|
+
export class SecurityHeadersMiddleware {
|
|
368
|
+
static beforeSecurity(req: Tina4Request, res: Tina4Response): [Tina4Request, Tina4Response] {
|
|
369
|
+
res.header(
|
|
370
|
+
"X-Frame-Options",
|
|
371
|
+
process.env.TINA4_FRAME_OPTIONS ?? "SAMEORIGIN",
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
res.header("X-Content-Type-Options", "nosniff");
|
|
375
|
+
|
|
376
|
+
const hsts = process.env.TINA4_HSTS ?? "";
|
|
377
|
+
if (hsts) {
|
|
378
|
+
res.header(
|
|
379
|
+
"Strict-Transport-Security",
|
|
380
|
+
`max-age=${hsts}; includeSubDomains`,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
res.header(
|
|
385
|
+
"Content-Security-Policy",
|
|
386
|
+
process.env.TINA4_CSP ?? "default-src 'self'",
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
res.header(
|
|
390
|
+
"Referrer-Policy",
|
|
391
|
+
process.env.TINA4_REFERRER_POLICY ?? "strict-origin-when-cross-origin",
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
res.header("X-XSS-Protection", "0");
|
|
395
|
+
|
|
396
|
+
res.header(
|
|
397
|
+
"Permissions-Policy",
|
|
398
|
+
process.env.TINA4_PERMISSIONS_POLICY ?? "camera=(), microphone=(), geolocation=()",
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
return [req, res];
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
352
405
|
// Built-in request logger middleware (function form — kept for backwards compat)
|
|
353
406
|
export function requestLogger(): Middleware {
|
|
354
407
|
return (req, res, next) => {
|
|
@@ -27,10 +27,27 @@ export function createRequest(req: IncomingMessage): Tina4Request {
|
|
|
27
27
|
return tReq;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/** Maximum upload size in bytes (default 10 MB). Override via TINA4_MAX_UPLOAD_SIZE env var. */
|
|
31
|
+
const TINA4_MAX_UPLOAD_SIZE = parseInt(process.env.TINA4_MAX_UPLOAD_SIZE ?? "10485760", 10);
|
|
32
|
+
|
|
33
|
+
export class PayloadTooLargeError extends Error {
|
|
34
|
+
public statusCode = 413;
|
|
35
|
+
constructor(actual: number, limit: number) {
|
|
36
|
+
super(`Request body (${actual} bytes) exceeds TINA4_MAX_UPLOAD_SIZE (${limit} bytes)`);
|
|
37
|
+
this.name = "PayloadTooLargeError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
export async function parseBody(req: Tina4Request): Promise<void> {
|
|
31
42
|
const method = req.method?.toUpperCase();
|
|
32
43
|
if (method === "GET" || method === "HEAD" || method === "OPTIONS") return;
|
|
33
44
|
|
|
45
|
+
// Check content-length header against upload size limit before reading body
|
|
46
|
+
const declaredLength = parseInt(req.headers["content-length"] ?? "0", 10);
|
|
47
|
+
if (declaredLength > TINA4_MAX_UPLOAD_SIZE) {
|
|
48
|
+
throw new PayloadTooLargeError(declaredLength, TINA4_MAX_UPLOAD_SIZE);
|
|
49
|
+
}
|
|
50
|
+
|
|
34
51
|
const contentType = req.headers["content-type"] ?? "";
|
|
35
52
|
const chunks: Buffer[] = [];
|
|
36
53
|
|
|
@@ -43,6 +60,11 @@ export async function parseBody(req: Tina4Request): Promise<void> {
|
|
|
43
60
|
const raw = Buffer.concat(chunks);
|
|
44
61
|
if (raw.length === 0) return;
|
|
45
62
|
|
|
63
|
+
// Check actual body size against upload size limit
|
|
64
|
+
if (raw.length > TINA4_MAX_UPLOAD_SIZE) {
|
|
65
|
+
throw new PayloadTooLargeError(raw.length, TINA4_MAX_UPLOAD_SIZE);
|
|
66
|
+
}
|
|
67
|
+
|
|
46
68
|
if (contentType.includes("multipart/form-data")) {
|
|
47
69
|
const boundary = extractBoundary(contentType);
|
|
48
70
|
if (boundary) {
|
|
@@ -128,6 +128,11 @@ export function createResponse(res: ServerResponse): Tina4Response {
|
|
|
128
128
|
return response.cookie(name, "", { ...options, maxAge: 0, expires: new Date(0) });
|
|
129
129
|
};
|
|
130
130
|
|
|
131
|
+
response.error = function (code: string, message: string, status?: number): Tina4Response {
|
|
132
|
+
const statusCode = status ?? 400;
|
|
133
|
+
return response.json({ error: true, code, message, status: statusCode }, statusCode);
|
|
134
|
+
};
|
|
135
|
+
|
|
131
136
|
response.file = function (filePath: string, options?: { download?: boolean; contentType?: string }): Tina4Response {
|
|
132
137
|
if (!fs.existsSync(filePath)) {
|
|
133
138
|
res.statusCode = 404;
|
|
@@ -190,3 +195,13 @@ export function createResponse(res: ServerResponse): Tina4Response {
|
|
|
190
195
|
|
|
191
196
|
return response;
|
|
192
197
|
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Build a standard error response envelope (standalone helper).
|
|
201
|
+
*
|
|
202
|
+
* Usage:
|
|
203
|
+
* return response(errorResponse("VALIDATION_FAILED", "Email is required", 400), 400);
|
|
204
|
+
*/
|
|
205
|
+
export function errorResponse(code: string, message: string, status: number = 400): Record<string, unknown> {
|
|
206
|
+
return { error: true, code, message, status };
|
|
207
|
+
}
|
|
@@ -37,6 +37,7 @@ export interface Tina4ResponseMethods {
|
|
|
37
37
|
cookie(name: string, value: string, options?: CookieOptions): Tina4Response;
|
|
38
38
|
clearCookie(name: string, options?: CookieOptions): Tina4Response;
|
|
39
39
|
file(path: string, options?: { download?: boolean; contentType?: string }): Tina4Response;
|
|
40
|
+
error(code: string, message: string, status?: number): Tina4Response;
|
|
40
41
|
render(template: string, data?: Record<string, unknown>): Promise<Tina4Response>;
|
|
41
42
|
template(name: string, data?: Record<string, unknown>): Promise<Tina4Response>;
|
|
42
43
|
/** The underlying ServerResponse for advanced use */
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tina4 Validator — Request body validation.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { Validator } from "@tina4/core";
|
|
6
|
+
*
|
|
7
|
+
* const validator = new Validator(req.body as Record<string, unknown>);
|
|
8
|
+
* validator.required("name", "email");
|
|
9
|
+
* validator.email("email");
|
|
10
|
+
* validator.minLength("name", 2);
|
|
11
|
+
* validator.maxLength("name", 100);
|
|
12
|
+
* validator.integer("age");
|
|
13
|
+
* validator.min("age", 0);
|
|
14
|
+
* validator.max("age", 150);
|
|
15
|
+
* validator.inList("role", ["admin", "user", "guest"]);
|
|
16
|
+
* validator.regex("phone", /^\+?[\d\s\-]+$/);
|
|
17
|
+
*
|
|
18
|
+
* if (!validator.isValid()) {
|
|
19
|
+
* return res.error("VALIDATION_FAILED", validator.errors()[0].message, 400);
|
|
20
|
+
* }
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export interface ValidationError {
|
|
24
|
+
field: string;
|
|
25
|
+
message: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const EMAIL_REGEX = /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/;
|
|
29
|
+
|
|
30
|
+
export class Validator {
|
|
31
|
+
private data: Record<string, unknown>;
|
|
32
|
+
private validationErrors: ValidationError[] = [];
|
|
33
|
+
|
|
34
|
+
constructor(data?: Record<string, unknown> | null) {
|
|
35
|
+
this.data = data && typeof data === "object" && !Array.isArray(data)
|
|
36
|
+
? data as Record<string, unknown>
|
|
37
|
+
: {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Check that one or more fields are present and non-empty. */
|
|
41
|
+
required(...fields: string[]): this {
|
|
42
|
+
for (const field of fields) {
|
|
43
|
+
const value = this.data[field];
|
|
44
|
+
if (value === undefined || value === null || (typeof value === "string" && value.trim() === "")) {
|
|
45
|
+
this.validationErrors.push({ field, message: `${field} is required` });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Check that a field contains a valid email address. */
|
|
52
|
+
email(field: string): this {
|
|
53
|
+
const value = this.data[field];
|
|
54
|
+
if (value === undefined || value === null) return this;
|
|
55
|
+
if (typeof value !== "string" || !EMAIL_REGEX.test(value)) {
|
|
56
|
+
this.validationErrors.push({ field, message: `${field} must be a valid email address` });
|
|
57
|
+
}
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Check that a string field has at least `length` characters. */
|
|
62
|
+
minLength(field: string, length: number): this {
|
|
63
|
+
const value = this.data[field];
|
|
64
|
+
if (value === undefined || value === null) return this;
|
|
65
|
+
if (typeof value !== "string" || value.length < length) {
|
|
66
|
+
this.validationErrors.push({ field, message: `${field} must be at least ${length} characters` });
|
|
67
|
+
}
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Check that a string field has at most `length` characters. */
|
|
72
|
+
maxLength(field: string, length: number): this {
|
|
73
|
+
const value = this.data[field];
|
|
74
|
+
if (value === undefined || value === null) return this;
|
|
75
|
+
if (typeof value !== "string" || value.length > length) {
|
|
76
|
+
this.validationErrors.push({ field, message: `${field} must be at most ${length} characters` });
|
|
77
|
+
}
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Check that a field is an integer (or can be parsed as one). */
|
|
82
|
+
integer(field: string): this {
|
|
83
|
+
const value = this.data[field];
|
|
84
|
+
if (value === undefined || value === null) return this;
|
|
85
|
+
if (typeof value === "boolean" || !Number.isInteger(Number(value)) || String(value).trim() === "") {
|
|
86
|
+
this.validationErrors.push({ field, message: `${field} must be an integer` });
|
|
87
|
+
}
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Check that a numeric field is >= `minimum`. */
|
|
92
|
+
min(field: string, minimum: number): this {
|
|
93
|
+
const value = this.data[field];
|
|
94
|
+
if (value === undefined || value === null) return this;
|
|
95
|
+
const num = Number(value);
|
|
96
|
+
if (isNaN(num)) return this;
|
|
97
|
+
if (num < minimum) {
|
|
98
|
+
this.validationErrors.push({ field, message: `${field} must be at least ${minimum}` });
|
|
99
|
+
}
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Check that a numeric field is <= `maximum`. */
|
|
104
|
+
max(field: string, maximum: number): this {
|
|
105
|
+
const value = this.data[field];
|
|
106
|
+
if (value === undefined || value === null) return this;
|
|
107
|
+
const num = Number(value);
|
|
108
|
+
if (isNaN(num)) return this;
|
|
109
|
+
if (num > maximum) {
|
|
110
|
+
this.validationErrors.push({ field, message: `${field} must be at most ${maximum}` });
|
|
111
|
+
}
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Check that a field's value is one of the allowed values. */
|
|
116
|
+
inList(field: string, allowed: unknown[]): this {
|
|
117
|
+
const value = this.data[field];
|
|
118
|
+
if (value === undefined || value === null) return this;
|
|
119
|
+
if (!allowed.includes(value)) {
|
|
120
|
+
this.validationErrors.push({ field, message: `${field} must be one of ${JSON.stringify(allowed)}` });
|
|
121
|
+
}
|
|
122
|
+
return this;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Check that a field matches a regular expression. */
|
|
126
|
+
regex(field: string, pattern: RegExp | string): this {
|
|
127
|
+
const value = this.data[field];
|
|
128
|
+
if (value === undefined || value === null) return this;
|
|
129
|
+
const re = pattern instanceof RegExp ? pattern : new RegExp(pattern);
|
|
130
|
+
if (typeof value !== "string" || !re.test(value)) {
|
|
131
|
+
this.validationErrors.push({ field, message: `${field} does not match the required format` });
|
|
132
|
+
}
|
|
133
|
+
return this;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Return the list of validation errors (empty if valid). */
|
|
137
|
+
errors(): ValidationError[] {
|
|
138
|
+
return [...this.validationErrors];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Return true if no validation errors have been recorded. */
|
|
142
|
+
isValid(): boolean {
|
|
143
|
+
return this.validationErrors.length === 0;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -185,9 +185,25 @@ export function parseDatabaseUrl(url: string, username?: string, password?: stri
|
|
|
185
185
|
* db.update("users", { name: "Bob" }, { id: 1 });
|
|
186
186
|
* db.delete("users", { id: 1 });
|
|
187
187
|
* db.close();
|
|
188
|
+
*
|
|
189
|
+
* Connection pooling:
|
|
190
|
+
* const db = await Database.create("sqlite:///data/app.db", undefined, undefined, 4);
|
|
191
|
+
* // 4 connections, round-robin rotation
|
|
188
192
|
*/
|
|
189
193
|
export class Database {
|
|
190
|
-
private adapter: DatabaseAdapter;
|
|
194
|
+
private adapter: DatabaseAdapter | null;
|
|
195
|
+
|
|
196
|
+
/** Connection pool — array of adapters with lazy creation */
|
|
197
|
+
private pool: (DatabaseAdapter | null)[] = [];
|
|
198
|
+
|
|
199
|
+
/** Pool size (0 = single connection) */
|
|
200
|
+
private poolSize: number = 0;
|
|
201
|
+
|
|
202
|
+
/** Round-robin index */
|
|
203
|
+
private poolIndex: number = 0;
|
|
204
|
+
|
|
205
|
+
/** Factory for creating new adapters (used by pool) */
|
|
206
|
+
private adapterFactory: (() => Promise<DatabaseAdapter>) | null = null;
|
|
191
207
|
|
|
192
208
|
/**
|
|
193
209
|
* Create a Database wrapping an existing adapter.
|
|
@@ -201,8 +217,33 @@ export class Database {
|
|
|
201
217
|
/**
|
|
202
218
|
* Async factory: creates a Database from a connection URL.
|
|
203
219
|
* Works with all adapter types (sqlite, postgres, mysql, mssql, firebird).
|
|
220
|
+
*
|
|
221
|
+
* @param url - Connection URL
|
|
222
|
+
* @param username - Optional username
|
|
223
|
+
* @param password - Optional password
|
|
224
|
+
* @param pool - Number of pooled connections (0 = single, N>0 = round-robin)
|
|
204
225
|
*/
|
|
205
|
-
static async create(url: string, username?: string, password?: string): Promise<Database> {
|
|
226
|
+
static async create(url: string, username?: string, password?: string, pool: number = 0): Promise<Database> {
|
|
227
|
+
if (pool > 0) {
|
|
228
|
+
// Pooled mode — create all adapters eagerly
|
|
229
|
+
const adapters: DatabaseAdapter[] = [];
|
|
230
|
+
for (let i = 0; i < pool; i++) {
|
|
231
|
+
adapters.push(await createAdapterFromUrl(url, username, password));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Set the first adapter as the global default
|
|
235
|
+
setAdapter(adapters[0]);
|
|
236
|
+
|
|
237
|
+
const db = new Database(adapters[0]);
|
|
238
|
+
db.poolSize = pool;
|
|
239
|
+
db.pool = adapters;
|
|
240
|
+
db.poolIndex = 0;
|
|
241
|
+
db.adapter = null; // Don't use single-adapter path
|
|
242
|
+
db.adapterFactory = () => createAdapterFromUrl(url, username, password);
|
|
243
|
+
return db;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Single-connection mode — current behavior
|
|
206
247
|
const adapter = await createAdapterFromUrl(url, username, password);
|
|
207
248
|
setAdapter(adapter);
|
|
208
249
|
return new Database(adapter);
|
|
@@ -211,84 +252,119 @@ export class Database {
|
|
|
211
252
|
/**
|
|
212
253
|
* Create a Database from an environment variable.
|
|
213
254
|
* @param envKey - Name of the env var holding the connection URL. Defaults to "DATABASE_URL".
|
|
255
|
+
* @param pool - Number of pooled connections (0 = single, N>0 = round-robin)
|
|
214
256
|
*/
|
|
215
|
-
static async fromEnv(envKey = "DATABASE_URL"): Promise<Database> {
|
|
257
|
+
static async fromEnv(envKey = "DATABASE_URL", pool: number = 0): Promise<Database> {
|
|
216
258
|
const url = process.env[envKey];
|
|
217
259
|
if (!url) {
|
|
218
260
|
throw new Error(`Environment variable "${envKey}" is not set.`);
|
|
219
261
|
}
|
|
220
|
-
return Database.create(url);
|
|
262
|
+
return Database.create(url, undefined, undefined, pool);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get the next adapter — from pool (round-robin) or single connection.
|
|
267
|
+
*/
|
|
268
|
+
private getNextAdapter(): DatabaseAdapter {
|
|
269
|
+
if (this.poolSize > 0) {
|
|
270
|
+
const idx = this.poolIndex;
|
|
271
|
+
this.poolIndex = (this.poolIndex + 1) % this.poolSize;
|
|
272
|
+
return this.pool[idx] as DatabaseAdapter;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return this.adapter!;
|
|
221
276
|
}
|
|
222
277
|
|
|
223
278
|
/** Get the underlying adapter (for advanced / escape-hatch usage). */
|
|
224
279
|
getAdapter(): DatabaseAdapter {
|
|
225
|
-
return this.
|
|
280
|
+
return this.getNextAdapter();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Get the pool size (0 = single connection mode). */
|
|
284
|
+
getPoolSize(): number {
|
|
285
|
+
return this.poolSize;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Get the number of active (created) connections in the pool. */
|
|
289
|
+
getActivePoolCount(): number {
|
|
290
|
+
if (this.poolSize === 0) return this.adapter ? 1 : 0;
|
|
291
|
+
return this.pool.filter(a => a !== null).length;
|
|
226
292
|
}
|
|
227
293
|
|
|
228
294
|
/** Query rows with optional pagination. Returns a DatabaseResult wrapper. */
|
|
229
295
|
fetch(sql: string, params?: unknown[], limit?: number, offset?: number): DatabaseResult {
|
|
230
|
-
const
|
|
231
|
-
|
|
296
|
+
const adapter = this.getNextAdapter();
|
|
297
|
+
const rows = adapter.fetch<Record<string, unknown>>(sql, params, limit, offset);
|
|
298
|
+
return new DatabaseResult(rows, undefined, undefined, limit, offset, adapter, sql);
|
|
232
299
|
}
|
|
233
300
|
|
|
234
301
|
/** Fetch a single row or null. */
|
|
235
302
|
fetchOne<T = Record<string, unknown>>(sql: string, params?: unknown[]): T | null {
|
|
236
|
-
return this.
|
|
303
|
+
return this.getNextAdapter().fetchOne<T>(sql, params);
|
|
237
304
|
}
|
|
238
305
|
|
|
239
306
|
/** Execute a statement (INSERT, UPDATE, DELETE, DDL). */
|
|
240
307
|
execute(sql: string, params?: unknown[]): unknown {
|
|
241
|
-
return this.
|
|
308
|
+
return this.getNextAdapter().execute(sql, params);
|
|
242
309
|
}
|
|
243
310
|
|
|
244
311
|
/** Insert a row into a table. */
|
|
245
312
|
insert(table: string, data: Record<string, unknown>): DatabaseWriteResult {
|
|
246
|
-
return this.
|
|
313
|
+
return this.getNextAdapter().insert(table, data);
|
|
247
314
|
}
|
|
248
315
|
|
|
249
316
|
/** Update rows in a table matching filter. */
|
|
250
317
|
update(table: string, data: Record<string, unknown>, filter?: Record<string, unknown>): DatabaseWriteResult {
|
|
251
|
-
return this.
|
|
318
|
+
return this.getNextAdapter().update(table, data, filter ?? {});
|
|
252
319
|
}
|
|
253
320
|
|
|
254
321
|
/** Delete rows from a table matching filter. */
|
|
255
322
|
delete(table: string, filter?: Record<string, unknown>): DatabaseWriteResult {
|
|
256
|
-
return this.
|
|
323
|
+
return this.getNextAdapter().delete(table, filter ?? {});
|
|
257
324
|
}
|
|
258
325
|
|
|
259
|
-
/** Close
|
|
326
|
+
/** Close all database connections (pool or single). */
|
|
260
327
|
close(): void {
|
|
261
|
-
this.
|
|
328
|
+
if (this.poolSize > 0) {
|
|
329
|
+
for (let i = 0; i < this.pool.length; i++) {
|
|
330
|
+
if (this.pool[i] !== null) {
|
|
331
|
+
this.pool[i]!.close();
|
|
332
|
+
this.pool[i] = null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
} else if (this.adapter) {
|
|
336
|
+
this.adapter.close();
|
|
337
|
+
}
|
|
262
338
|
}
|
|
263
339
|
|
|
264
340
|
/** Start a transaction. */
|
|
265
341
|
startTransaction(): void {
|
|
266
|
-
this.
|
|
342
|
+
this.getNextAdapter().startTransaction();
|
|
267
343
|
}
|
|
268
344
|
|
|
269
345
|
/** Commit the current transaction. */
|
|
270
346
|
commit(): void {
|
|
271
|
-
this.
|
|
347
|
+
this.getNextAdapter().commit();
|
|
272
348
|
}
|
|
273
349
|
|
|
274
350
|
/** Rollback the current transaction. */
|
|
275
351
|
rollback(): void {
|
|
276
|
-
this.
|
|
352
|
+
this.getNextAdapter().rollback();
|
|
277
353
|
}
|
|
278
354
|
|
|
279
355
|
/** Check if a table exists. */
|
|
280
356
|
tableExists(name: string): boolean {
|
|
281
|
-
return this.
|
|
357
|
+
return this.getNextAdapter().tableExists(name);
|
|
282
358
|
}
|
|
283
359
|
|
|
284
360
|
/** List all tables in the database. */
|
|
285
361
|
getTables(): string[] {
|
|
286
|
-
return this.
|
|
362
|
+
return this.getNextAdapter().tables();
|
|
287
363
|
}
|
|
288
364
|
|
|
289
365
|
/** Get the last auto-increment id. */
|
|
290
366
|
getLastId(): string | number {
|
|
291
|
-
const id = this.
|
|
367
|
+
const id = this.getNextAdapter().lastInsertId();
|
|
292
368
|
if (id === null) return 0;
|
|
293
369
|
return typeof id === "bigint" ? id.toString() : id;
|
|
294
370
|
}
|