tina4-nodejs 3.13.31 → 3.13.32
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 +1 -1
- package/package.json +1 -1
- package/packages/core/src/cache.ts +4 -0
- package/packages/core/src/index.ts +2 -1
- package/packages/core/src/router.ts +89 -37
- package/packages/core/src/types.ts +16 -1
- package/packages/orm/src/cachedDatabase.ts +15 -8
- package/packages/orm/src/database.ts +26 -11
package/CLAUDE.md
CHANGED
package/package.json
CHANGED
|
@@ -1269,6 +1269,9 @@ export function responseCache(config?: ResponseCacheConfig): Middleware {
|
|
|
1269
1269
|
if (cached && typeof cached === "object" && typeof cached.body === "string") {
|
|
1270
1270
|
// Cache HIT — serve from the (possibly distributed) backend.
|
|
1271
1271
|
res.header("X-Cache", "HIT");
|
|
1272
|
+
// X-Cache-TTL advertises the configured cache lifetime in seconds
|
|
1273
|
+
// (parity with Python/PHP/Ruby, which set it alongside X-Cache).
|
|
1274
|
+
res.header("X-Cache-TTL", String(ttl));
|
|
1272
1275
|
res.header("Content-Type", cached.contentType);
|
|
1273
1276
|
res(cached.body, cached.statusCode, cached.contentType);
|
|
1274
1277
|
return;
|
|
@@ -1297,6 +1300,7 @@ export function responseCache(config?: ResponseCacheConfig): Middleware {
|
|
|
1297
1300
|
}
|
|
1298
1301
|
|
|
1299
1302
|
res.header("X-Cache", "MISS");
|
|
1303
|
+
res.header("X-Cache-TTL", String(ttl));
|
|
1300
1304
|
return originalEnd(chunk, ...args);
|
|
1301
1305
|
} as any;
|
|
1302
1306
|
|
|
@@ -6,6 +6,7 @@ export type {
|
|
|
6
6
|
RouteMeta,
|
|
7
7
|
Tina4Config,
|
|
8
8
|
Middleware,
|
|
9
|
+
MiddlewareSpec,
|
|
9
10
|
UploadedFile,
|
|
10
11
|
CookieOptions,
|
|
11
12
|
WebSocketRouteHandler,
|
|
@@ -14,7 +15,7 @@ export type {
|
|
|
14
15
|
|
|
15
16
|
export { startServer, resolvePortAndHost, handle, start, stop, httpReason, resolveTemplate, resetTemplateCache, templateAutoRoutingEnabled, isBannerSuppressed } from "./server.js";
|
|
16
17
|
export { background, stopAllBackgroundTasks, backgroundTaskCount } from "./background.js";
|
|
17
|
-
export { Router, RouteGroup, RouteRef, defaultRouter, runRouteMiddlewares, isTrailingSlashRedirectEnabled } from "./router.js";
|
|
18
|
+
export { Router, RouteGroup, RouteRef, defaultRouter, runRouteMiddlewares, resolveStringMiddleware, isTrailingSlashRedirectEnabled } from "./router.js";
|
|
18
19
|
export { get, post, put, patch, del, any, websocket, del as delete } from "./router.js";
|
|
19
20
|
export type { RouteInfo } from "./router.js";
|
|
20
21
|
export { discoverRoutes } from "./routeDiscovery.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RouteHandler, RouteDefinition, RouteMeta, Middleware, Tina4Request, Tina4Response, WebSocketRouteHandler, WebSocketRouteDefinition } from "./types.js";
|
|
1
|
+
import type { RouteHandler, RouteDefinition, RouteMeta, Middleware, MiddlewareSpec, Tina4Request, Tina4Response, WebSocketRouteHandler, WebSocketRouteDefinition } from "./types.js";
|
|
2
2
|
import { isTruthy } from "./dotenv.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -43,7 +43,7 @@ interface MatchResult {
|
|
|
43
43
|
params: Record<string, string | number>;
|
|
44
44
|
pattern: string;
|
|
45
45
|
meta?: RouteMeta;
|
|
46
|
-
middlewares?:
|
|
46
|
+
middlewares?: MiddlewareSpec[];
|
|
47
47
|
template?: string;
|
|
48
48
|
secure?: boolean;
|
|
49
49
|
cached?: boolean;
|
|
@@ -58,7 +58,7 @@ interface CompiledRoute {
|
|
|
58
58
|
handler: RouteHandler;
|
|
59
59
|
meta?: RouteMeta;
|
|
60
60
|
filePath?: string;
|
|
61
|
-
middlewares?:
|
|
61
|
+
middlewares?: MiddlewareSpec[];
|
|
62
62
|
secure?: boolean;
|
|
63
63
|
cached?: boolean;
|
|
64
64
|
noAuth?: boolean;
|
|
@@ -94,8 +94,11 @@ export class RouteRef {
|
|
|
94
94
|
return this;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
/**
|
|
98
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Append middleware to this route. Accepts middleware functions and/or
|
|
99
|
+
* string specs (e.g. `"ResponseCache:300"`), resolved when the route runs.
|
|
100
|
+
*/
|
|
101
|
+
middleware(...middlewareClasses: MiddlewareSpec[]): this {
|
|
99
102
|
this.route.middlewares = [...(this.route.middlewares ?? []), ...middlewareClasses];
|
|
100
103
|
return this;
|
|
101
104
|
}
|
|
@@ -187,35 +190,35 @@ export class Router {
|
|
|
187
190
|
/**
|
|
188
191
|
* Register a GET route programmatically.
|
|
189
192
|
*/
|
|
190
|
-
get(path: string, handler: RouteHandler, middlewares?:
|
|
193
|
+
get(path: string, handler: RouteHandler, middlewares?: MiddlewareSpec[], meta?: RouteMeta): RouteRef {
|
|
191
194
|
return this.addRoute({ method: "GET", pattern: path, handler, middlewares, meta });
|
|
192
195
|
}
|
|
193
196
|
|
|
194
197
|
/**
|
|
195
198
|
* Register a POST route programmatically.
|
|
196
199
|
*/
|
|
197
|
-
post(path: string, handler: RouteHandler, middlewares?:
|
|
200
|
+
post(path: string, handler: RouteHandler, middlewares?: MiddlewareSpec[], meta?: RouteMeta): RouteRef {
|
|
198
201
|
return this.addRoute({ method: "POST", pattern: path, handler, middlewares, meta });
|
|
199
202
|
}
|
|
200
203
|
|
|
201
204
|
/**
|
|
202
205
|
* Register a PUT route programmatically.
|
|
203
206
|
*/
|
|
204
|
-
put(path: string, handler: RouteHandler, middlewares?:
|
|
207
|
+
put(path: string, handler: RouteHandler, middlewares?: MiddlewareSpec[], meta?: RouteMeta): RouteRef {
|
|
205
208
|
return this.addRoute({ method: "PUT", pattern: path, handler, middlewares, meta });
|
|
206
209
|
}
|
|
207
210
|
|
|
208
211
|
/**
|
|
209
212
|
* Register a PATCH route programmatically.
|
|
210
213
|
*/
|
|
211
|
-
patch(path: string, handler: RouteHandler, middlewares?:
|
|
214
|
+
patch(path: string, handler: RouteHandler, middlewares?: MiddlewareSpec[], meta?: RouteMeta): RouteRef {
|
|
212
215
|
return this.addRoute({ method: "PATCH", pattern: path, handler, middlewares, meta });
|
|
213
216
|
}
|
|
214
217
|
|
|
215
218
|
/**
|
|
216
219
|
* Register a DELETE route programmatically.
|
|
217
220
|
*/
|
|
218
|
-
delete(path: string, handler: RouteHandler, middlewares?:
|
|
221
|
+
delete(path: string, handler: RouteHandler, middlewares?: MiddlewareSpec[], meta?: RouteMeta): RouteRef {
|
|
219
222
|
return this.addRoute({ method: "DELETE", pattern: path, handler, middlewares, meta });
|
|
220
223
|
}
|
|
221
224
|
|
|
@@ -227,7 +230,7 @@ export class Router {
|
|
|
227
230
|
* logic, custom validator headers without the cost of building the body.
|
|
228
231
|
* The framework still strips the response body for you on the way out.
|
|
229
232
|
*/
|
|
230
|
-
head(path: string, handler: RouteHandler, middlewares?:
|
|
233
|
+
head(path: string, handler: RouteHandler, middlewares?: MiddlewareSpec[], meta?: RouteMeta): RouteRef {
|
|
231
234
|
return this.addRoute({ method: "HEAD", pattern: path, handler, middlewares, meta });
|
|
232
235
|
}
|
|
233
236
|
|
|
@@ -237,14 +240,14 @@ export class Router {
|
|
|
237
240
|
* registered for the path and returning 204 (RFC 9110 §9.3.7). Use
|
|
238
241
|
* this to take over that behaviour.
|
|
239
242
|
*/
|
|
240
|
-
options(path: string, handler: RouteHandler, middlewares?:
|
|
243
|
+
options(path: string, handler: RouteHandler, middlewares?: MiddlewareSpec[], meta?: RouteMeta): RouteRef {
|
|
241
244
|
return this.addRoute({ method: "OPTIONS", pattern: path, handler, middlewares, meta });
|
|
242
245
|
}
|
|
243
246
|
|
|
244
247
|
/**
|
|
245
248
|
* Register a route that matches ANY HTTP method.
|
|
246
249
|
*/
|
|
247
|
-
any(path: string, handler: RouteHandler, middlewares?:
|
|
250
|
+
any(path: string, handler: RouteHandler, middlewares?: MiddlewareSpec[], meta?: RouteMeta): RouteRef {
|
|
248
251
|
let lastRef!: RouteRef;
|
|
249
252
|
for (const method of ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]) {
|
|
250
253
|
lastRef = this.addRoute({ method, pattern: path, handler, middlewares, meta });
|
|
@@ -255,7 +258,7 @@ export class Router {
|
|
|
255
258
|
/**
|
|
256
259
|
* Create a route group with a shared prefix and optional middlewares.
|
|
257
260
|
*/
|
|
258
|
-
group(prefix: string, callback: (group: RouteGroup) => void, middlewares?:
|
|
261
|
+
group(prefix: string, callback: (group: RouteGroup) => void, middlewares?: MiddlewareSpec[]): void {
|
|
259
262
|
const group = new RouteGroup(this, prefix, middlewares);
|
|
260
263
|
callback(group);
|
|
261
264
|
}
|
|
@@ -447,7 +450,7 @@ export class Router {
|
|
|
447
450
|
* Register a route for a specific HTTP method.
|
|
448
451
|
* Core registration method — all convenience methods delegate here.
|
|
449
452
|
*/
|
|
450
|
-
static add(method: string, path: string, handler: RouteHandler, middleware?:
|
|
453
|
+
static add(method: string, path: string, handler: RouteHandler, middleware?: MiddlewareSpec[], swaggerMeta?: RouteMeta, template?: string): RouteRef {
|
|
451
454
|
const m = method.toUpperCase();
|
|
452
455
|
if (m === "ANY") {
|
|
453
456
|
return defaultRouter.any(path, handler, middleware, swaggerMeta);
|
|
@@ -458,42 +461,42 @@ export class Router {
|
|
|
458
461
|
/**
|
|
459
462
|
* Register a GET route on the default global router.
|
|
460
463
|
*/
|
|
461
|
-
static get(path: string, handler: RouteHandler, middleware?:
|
|
464
|
+
static get(path: string, handler: RouteHandler, middleware?: MiddlewareSpec[], swaggerMeta?: RouteMeta, template?: string): RouteRef {
|
|
462
465
|
return defaultRouter.get(path, handler, middleware, swaggerMeta);
|
|
463
466
|
}
|
|
464
467
|
|
|
465
468
|
/**
|
|
466
469
|
* Register a POST route on the default global router.
|
|
467
470
|
*/
|
|
468
|
-
static post(path: string, handler: RouteHandler, middleware?:
|
|
471
|
+
static post(path: string, handler: RouteHandler, middleware?: MiddlewareSpec[], swaggerMeta?: RouteMeta, template?: string): RouteRef {
|
|
469
472
|
return defaultRouter.post(path, handler, middleware, swaggerMeta);
|
|
470
473
|
}
|
|
471
474
|
|
|
472
475
|
/**
|
|
473
476
|
* Register a PUT route on the default global router.
|
|
474
477
|
*/
|
|
475
|
-
static put(path: string, handler: RouteHandler, middleware?:
|
|
478
|
+
static put(path: string, handler: RouteHandler, middleware?: MiddlewareSpec[], swaggerMeta?: RouteMeta, template?: string): RouteRef {
|
|
476
479
|
return defaultRouter.put(path, handler, middleware, swaggerMeta);
|
|
477
480
|
}
|
|
478
481
|
|
|
479
482
|
/**
|
|
480
483
|
* Register a PATCH route on the default global router.
|
|
481
484
|
*/
|
|
482
|
-
static patch(path: string, handler: RouteHandler, middleware?:
|
|
485
|
+
static patch(path: string, handler: RouteHandler, middleware?: MiddlewareSpec[], swaggerMeta?: RouteMeta, template?: string): RouteRef {
|
|
483
486
|
return defaultRouter.patch(path, handler, middleware, swaggerMeta);
|
|
484
487
|
}
|
|
485
488
|
|
|
486
489
|
/**
|
|
487
490
|
* Register a DELETE route on the default global router.
|
|
488
491
|
*/
|
|
489
|
-
static delete(path: string, handler: RouteHandler, middleware?:
|
|
492
|
+
static delete(path: string, handler: RouteHandler, middleware?: MiddlewareSpec[], swaggerMeta?: RouteMeta, template?: string): RouteRef {
|
|
490
493
|
return defaultRouter.delete(path, handler, middleware, swaggerMeta);
|
|
491
494
|
}
|
|
492
495
|
|
|
493
496
|
/**
|
|
494
497
|
* Register a route that matches ANY HTTP method on the default global router.
|
|
495
498
|
*/
|
|
496
|
-
static any(path: string, handler: RouteHandler, middleware?:
|
|
499
|
+
static any(path: string, handler: RouteHandler, middleware?: MiddlewareSpec[], swaggerMeta?: RouteMeta, template?: string): RouteRef {
|
|
497
500
|
return defaultRouter.any(path, handler, middleware, swaggerMeta);
|
|
498
501
|
}
|
|
499
502
|
|
|
@@ -507,7 +510,7 @@ export class Router {
|
|
|
507
510
|
/**
|
|
508
511
|
* Create a route group on the default global router.
|
|
509
512
|
*/
|
|
510
|
-
static group(prefix: string, callback: (group: RouteGroup) => void, middlewares?:
|
|
513
|
+
static group(prefix: string, callback: (group: RouteGroup) => void, middlewares?: MiddlewareSpec[]): void {
|
|
511
514
|
defaultRouter.group(prefix, callback, middlewares);
|
|
512
515
|
}
|
|
513
516
|
|
|
@@ -622,7 +625,7 @@ export class RouteGroup {
|
|
|
622
625
|
return merged.length > 0 ? merged : undefined;
|
|
623
626
|
}
|
|
624
627
|
|
|
625
|
-
get(path: string, handler: RouteHandler, middlewares?:
|
|
628
|
+
get(path: string, handler: RouteHandler, middlewares?: MiddlewareSpec[], meta?: RouteMeta): RouteRef {
|
|
626
629
|
return this.router.addRoute({
|
|
627
630
|
method: "GET",
|
|
628
631
|
pattern: this.prefix + path,
|
|
@@ -632,7 +635,7 @@ export class RouteGroup {
|
|
|
632
635
|
});
|
|
633
636
|
}
|
|
634
637
|
|
|
635
|
-
post(path: string, handler: RouteHandler, middlewares?:
|
|
638
|
+
post(path: string, handler: RouteHandler, middlewares?: MiddlewareSpec[], meta?: RouteMeta): RouteRef {
|
|
636
639
|
return this.router.addRoute({
|
|
637
640
|
method: "POST",
|
|
638
641
|
pattern: this.prefix + path,
|
|
@@ -642,7 +645,7 @@ export class RouteGroup {
|
|
|
642
645
|
});
|
|
643
646
|
}
|
|
644
647
|
|
|
645
|
-
put(path: string, handler: RouteHandler, middlewares?:
|
|
648
|
+
put(path: string, handler: RouteHandler, middlewares?: MiddlewareSpec[], meta?: RouteMeta): RouteRef {
|
|
646
649
|
return this.router.addRoute({
|
|
647
650
|
method: "PUT",
|
|
648
651
|
pattern: this.prefix + path,
|
|
@@ -652,7 +655,7 @@ export class RouteGroup {
|
|
|
652
655
|
});
|
|
653
656
|
}
|
|
654
657
|
|
|
655
|
-
patch(path: string, handler: RouteHandler, middlewares?:
|
|
658
|
+
patch(path: string, handler: RouteHandler, middlewares?: MiddlewareSpec[], meta?: RouteMeta): RouteRef {
|
|
656
659
|
return this.router.addRoute({
|
|
657
660
|
method: "PATCH",
|
|
658
661
|
pattern: this.prefix + path,
|
|
@@ -662,7 +665,7 @@ export class RouteGroup {
|
|
|
662
665
|
});
|
|
663
666
|
}
|
|
664
667
|
|
|
665
|
-
delete(path: string, handler: RouteHandler, middlewares?:
|
|
668
|
+
delete(path: string, handler: RouteHandler, middlewares?: MiddlewareSpec[], meta?: RouteMeta): RouteRef {
|
|
666
669
|
return this.router.addRoute({
|
|
667
670
|
method: "DELETE",
|
|
668
671
|
pattern: this.prefix + path,
|
|
@@ -672,7 +675,7 @@ export class RouteGroup {
|
|
|
672
675
|
});
|
|
673
676
|
}
|
|
674
677
|
|
|
675
|
-
any(path: string, handler: RouteHandler, middlewares?:
|
|
678
|
+
any(path: string, handler: RouteHandler, middlewares?: MiddlewareSpec[], meta?: RouteMeta): RouteRef {
|
|
676
679
|
let lastRef!: RouteRef;
|
|
677
680
|
for (const method of ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]) {
|
|
678
681
|
lastRef = this.router.addRoute({
|
|
@@ -689,7 +692,7 @@ export class RouteGroup {
|
|
|
689
692
|
/**
|
|
690
693
|
* Nested groups.
|
|
691
694
|
*/
|
|
692
|
-
group(prefix: string, callback: (group: RouteGroup) => void, middlewares?:
|
|
695
|
+
group(prefix: string, callback: (group: RouteGroup) => void, middlewares?: MiddlewareSpec[]): void {
|
|
693
696
|
const nestedGroup = new RouteGroup(
|
|
694
697
|
this.router,
|
|
695
698
|
this.prefix + prefix,
|
|
@@ -699,15 +702,64 @@ export class RouteGroup {
|
|
|
699
702
|
}
|
|
700
703
|
}
|
|
701
704
|
|
|
705
|
+
/**
|
|
706
|
+
* Resolve a string-form middleware spec to a middleware function.
|
|
707
|
+
*
|
|
708
|
+
* Forms (parity with Python/PHP/Ruby):
|
|
709
|
+
* "ResponseCache" → responseCache() with the default/env TTL
|
|
710
|
+
* "ResponseCache:300" → responseCache({ ttl: 300 })
|
|
711
|
+
*
|
|
712
|
+
* The head before the first ":" names the middleware; any trailing
|
|
713
|
+
* colon-separated parts are its arguments (numeric parts are parsed as
|
|
714
|
+
* integers). Unknown names throw so a typo surfaces instead of silently
|
|
715
|
+
* dropping the middleware. `responseCache` is loaded via a dynamic import so
|
|
716
|
+
* the router carries no import-time dependency on the cache module.
|
|
717
|
+
*
|
|
718
|
+
* Exported so route dispatch (and tests) can turn a spec into a runnable
|
|
719
|
+
* middleware.
|
|
720
|
+
*/
|
|
721
|
+
export async function resolveStringMiddleware(spec: string): Promise<Middleware> {
|
|
722
|
+
const colon = spec.indexOf(":");
|
|
723
|
+
const name = colon >= 0 ? spec.slice(0, colon) : spec;
|
|
724
|
+
const rawArgs = colon >= 0 ? spec.slice(colon + 1).split(":") : [];
|
|
725
|
+
|
|
726
|
+
switch (name) {
|
|
727
|
+
case "ResponseCache": {
|
|
728
|
+
const { responseCache } = await import("./cache.js");
|
|
729
|
+
// First arg (if any) is the TTL in seconds.
|
|
730
|
+
const ttlArg = rawArgs[0];
|
|
731
|
+
const ttl = ttlArg !== undefined && /^\d+$/.test(ttlArg) ? parseInt(ttlArg, 10) : undefined;
|
|
732
|
+
return responseCache(ttl !== undefined ? { ttl } : undefined);
|
|
733
|
+
}
|
|
734
|
+
default:
|
|
735
|
+
throw new Error(
|
|
736
|
+
`Unknown middleware "${name}". Known string middleware: ResponseCache. ` +
|
|
737
|
+
`For custom middleware, pass the function directly to .middleware(fn).`,
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Resolve a single route-middleware spec to a middleware function. Functions
|
|
744
|
+
* pass through unchanged; strings are resolved via resolveStringMiddleware.
|
|
745
|
+
*/
|
|
746
|
+
async function resolveMiddlewareSpec(spec: MiddlewareSpec): Promise<Middleware> {
|
|
747
|
+
return typeof spec === "string" ? resolveStringMiddleware(spec) : spec;
|
|
748
|
+
}
|
|
749
|
+
|
|
702
750
|
/**
|
|
703
751
|
* Run per-route middleware chain, then call the handler.
|
|
752
|
+
*
|
|
753
|
+
* Accepts middleware functions and/or string specs (e.g. "ResponseCache:300").
|
|
754
|
+
* Each spec is resolved to a middleware function just before it runs.
|
|
704
755
|
*/
|
|
705
756
|
export async function runRouteMiddlewares(
|
|
706
|
-
middlewares:
|
|
757
|
+
middlewares: MiddlewareSpec[],
|
|
707
758
|
req: Tina4Request,
|
|
708
759
|
res: Tina4Response,
|
|
709
760
|
): Promise<boolean> {
|
|
710
|
-
for (const
|
|
761
|
+
for (const spec of middlewares) {
|
|
762
|
+
const mw = await resolveMiddlewareSpec(spec);
|
|
711
763
|
let nextCalled = false;
|
|
712
764
|
await mw(req, res, () => {
|
|
713
765
|
nextCalled = true;
|
|
@@ -739,28 +791,28 @@ export const defaultRouter = new Router();
|
|
|
739
791
|
* res.json({ id: req.params.id }, 201);
|
|
740
792
|
* });
|
|
741
793
|
*/
|
|
742
|
-
export function get(path: string, handler: RouteHandler, middlewares?:
|
|
794
|
+
export function get(path: string, handler: RouteHandler, middlewares?: MiddlewareSpec[], meta?: RouteMeta): RouteRef {
|
|
743
795
|
return defaultRouter.get(path, handler, middlewares, meta);
|
|
744
796
|
}
|
|
745
797
|
|
|
746
|
-
export function post(path: string, handler: RouteHandler, middlewares?:
|
|
798
|
+
export function post(path: string, handler: RouteHandler, middlewares?: MiddlewareSpec[], meta?: RouteMeta): RouteRef {
|
|
747
799
|
return defaultRouter.post(path, handler, middlewares, meta);
|
|
748
800
|
}
|
|
749
801
|
|
|
750
|
-
export function put(path: string, handler: RouteHandler, middlewares?:
|
|
802
|
+
export function put(path: string, handler: RouteHandler, middlewares?: MiddlewareSpec[], meta?: RouteMeta): RouteRef {
|
|
751
803
|
return defaultRouter.put(path, handler, middlewares, meta);
|
|
752
804
|
}
|
|
753
805
|
|
|
754
|
-
export function patch(path: string, handler: RouteHandler, middlewares?:
|
|
806
|
+
export function patch(path: string, handler: RouteHandler, middlewares?: MiddlewareSpec[], meta?: RouteMeta): RouteRef {
|
|
755
807
|
return defaultRouter.patch(path, handler, middlewares, meta);
|
|
756
808
|
}
|
|
757
809
|
|
|
758
810
|
// Named "del" to avoid conflict with the "delete" keyword; also exported as "delete" alias below.
|
|
759
|
-
export function del(path: string, handler: RouteHandler, middlewares?:
|
|
811
|
+
export function del(path: string, handler: RouteHandler, middlewares?: MiddlewareSpec[], meta?: RouteMeta): RouteRef {
|
|
760
812
|
return defaultRouter.delete(path, handler, middlewares, meta);
|
|
761
813
|
}
|
|
762
814
|
|
|
763
|
-
export function any(path: string, handler: RouteHandler, middlewares?:
|
|
815
|
+
export function any(path: string, handler: RouteHandler, middlewares?: MiddlewareSpec[], meta?: RouteMeta): RouteRef {
|
|
764
816
|
return defaultRouter.any(path, handler, middlewares, meta);
|
|
765
817
|
}
|
|
766
818
|
|
|
@@ -117,7 +117,8 @@ export interface RouteDefinition {
|
|
|
117
117
|
handler: RouteHandler;
|
|
118
118
|
filePath?: string;
|
|
119
119
|
meta?: RouteMeta;
|
|
120
|
-
|
|
120
|
+
/** Middleware functions and/or string specs (e.g. "ResponseCache:300"). */
|
|
121
|
+
middlewares?: MiddlewareSpec[];
|
|
121
122
|
/** Template file to render when handler returns a plain object */
|
|
122
123
|
template?: string;
|
|
123
124
|
/** Whether this route requires bearer-token authentication */
|
|
@@ -158,6 +159,20 @@ export type Middleware = (
|
|
|
158
159
|
next: () => void
|
|
159
160
|
) => void | Promise<void>;
|
|
160
161
|
|
|
162
|
+
/**
|
|
163
|
+
* A route middleware entry: either a middleware function, or a string spec
|
|
164
|
+
* resolved by the router to a built-in middleware.
|
|
165
|
+
*
|
|
166
|
+
* String forms (parity with Python/PHP/Ruby):
|
|
167
|
+
* "ResponseCache" → responseCache() with the default/env TTL
|
|
168
|
+
* "ResponseCache:300" → responseCache({ ttl: 300 })
|
|
169
|
+
*
|
|
170
|
+
* The router resolves string specs to middleware functions when the route
|
|
171
|
+
* runs, so callers can register a response-cache middleware without importing
|
|
172
|
+
* `responseCache`.
|
|
173
|
+
*/
|
|
174
|
+
export type MiddlewareSpec = Middleware | string;
|
|
175
|
+
|
|
161
176
|
/**
|
|
162
177
|
* Handler for WebSocket routes.
|
|
163
178
|
* connection — object with send/broadcast/close methods and route params.
|
|
@@ -317,8 +317,12 @@ export class CachedDatabaseAdapter implements DatabaseAdapter {
|
|
|
317
317
|
return this.adapter.query(sql, params);
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
-
fetch<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): T[] {
|
|
321
|
-
|
|
320
|
+
fetch<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number, noCache?: boolean): T[] {
|
|
321
|
+
// `noCache` bypasses the query cache for this one call — no lookup, no
|
|
322
|
+
// store, run straight against the underlying adapter (mirrors the Python
|
|
323
|
+
// master's `no_cache`). Counters are left untouched so a bypass read isn't
|
|
324
|
+
// misreported as a hit or a miss.
|
|
325
|
+
if (this.enabled && !noCache) {
|
|
322
326
|
const key = QueryCache.queryKey(sql + `:L${limit}:S${skip}`, params as unknown[] | undefined);
|
|
323
327
|
const cached = this.cache.get<T[]>(key);
|
|
324
328
|
if (cached !== undefined) {
|
|
@@ -333,8 +337,8 @@ export class CachedDatabaseAdapter implements DatabaseAdapter {
|
|
|
333
337
|
return this.adapter.fetch(sql, params, limit, skip);
|
|
334
338
|
}
|
|
335
339
|
|
|
336
|
-
fetchOne<T = Record<string, unknown>>(sql: string, params?: unknown[]): T | null {
|
|
337
|
-
if (this.enabled) {
|
|
340
|
+
fetchOne<T = Record<string, unknown>>(sql: string, params?: unknown[], noCache?: boolean): T | null {
|
|
341
|
+
if (this.enabled && !noCache) {
|
|
338
342
|
const key = QueryCache.queryKey(sql + ":ONE", params as unknown[] | undefined);
|
|
339
343
|
const cached = this.cache.get<T | null>(key);
|
|
340
344
|
if (cached !== undefined) {
|
|
@@ -417,11 +421,13 @@ export class CachedDatabaseAdapter implements DatabaseAdapter {
|
|
|
417
421
|
// ORM read/write path prefer those when present. We mirror them here so the
|
|
418
422
|
// cache sits in front of the async path too. Reads cache; writes flush.
|
|
419
423
|
|
|
420
|
-
async fetchAsync<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): Promise<T[]> {
|
|
424
|
+
async fetchAsync<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number, noCache?: boolean): Promise<T[]> {
|
|
421
425
|
const run = async (): Promise<T[]> => (this.adapter as any).fetchAsync
|
|
422
426
|
? await (this.adapter as any).fetchAsync(sql, params, limit, skip)
|
|
423
427
|
: this.adapter.fetch<T>(sql, params, limit, skip);
|
|
424
|
-
|
|
428
|
+
// `noCache` bypasses both cache layers for this one call — no lookup, no
|
|
429
|
+
// store, run directly (mirrors the Python master's `no_cache`).
|
|
430
|
+
if (this.enabled && !noCache) {
|
|
425
431
|
const key = QueryCache.queryKey(sql + `:L${limit}:S${skip}`, params as unknown[] | undefined);
|
|
426
432
|
// Persistent distributed backend is AUTHORITATIVE (mirrors Python, where a
|
|
427
433
|
// configured _cache_backend bypasses the in-process dict). This keeps
|
|
@@ -448,11 +454,12 @@ export class CachedDatabaseAdapter implements DatabaseAdapter {
|
|
|
448
454
|
return run();
|
|
449
455
|
}
|
|
450
456
|
|
|
451
|
-
async fetchOneAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T | null> {
|
|
457
|
+
async fetchOneAsync<T = Record<string, unknown>>(sql: string, params?: unknown[], noCache?: boolean): Promise<T | null> {
|
|
452
458
|
const run = async (): Promise<T | null> => (this.adapter as any).fetchOneAsync
|
|
453
459
|
? await (this.adapter as any).fetchOneAsync(sql, params)
|
|
454
460
|
: this.adapter.fetchOne<T>(sql, params);
|
|
455
|
-
|
|
461
|
+
// `noCache` bypasses both cache layers for this one call (see fetchAsync).
|
|
462
|
+
if (this.enabled && !noCache) {
|
|
456
463
|
const key = QueryCache.queryKey(sql + ":ONE", params as unknown[] | undefined);
|
|
457
464
|
if (this.usesPersistentBackend()) {
|
|
458
465
|
const shared = await this.backendGetOne<T>(key);
|
|
@@ -38,11 +38,13 @@ export function stripTrailingSemicolons(sql: string): string {
|
|
|
38
38
|
* the single chokepoint every public read/write flows through.
|
|
39
39
|
*/
|
|
40
40
|
export async function adapterFetch<T = Record<string, unknown>>(
|
|
41
|
-
adapter: DatabaseAdapter, sql: string, params?: unknown[], limit?: number, skip?: number,
|
|
41
|
+
adapter: DatabaseAdapter, sql: string, params?: unknown[], limit?: number, skip?: number, noCache?: boolean,
|
|
42
42
|
): Promise<T[]> {
|
|
43
|
+
// `noCache` is forwarded to the CachedDatabaseAdapter so a single read can
|
|
44
|
+
// bypass the query cache; raw adapters ignore the extra trailing arg.
|
|
43
45
|
return (adapter as any).fetchAsync
|
|
44
|
-
? await (adapter as any).fetchAsync(sql, params, limit, skip)
|
|
45
|
-
: adapter.fetch<T>(sql, params, limit, skip);
|
|
46
|
+
? await (adapter as any).fetchAsync(sql, params, limit, skip, noCache)
|
|
47
|
+
: adapter.fetch<T>(sql, params, limit, skip, noCache);
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
export async function adapterQuery<T = Record<string, unknown>>(
|
|
@@ -633,14 +635,16 @@ export class Database {
|
|
|
633
635
|
* the fallback resolves instantly). This is the breaking change that makes
|
|
634
636
|
* the wrapper work uniformly across every engine.
|
|
635
637
|
*/
|
|
636
|
-
async fetch(sql: string, params?: unknown[], limit?: number, offset?: number): Promise<DatabaseResult> {
|
|
638
|
+
async fetch(sql: string, params?: unknown[], limit?: number, offset?: number, opts?: { noCache?: boolean }): Promise<DatabaseResult> {
|
|
637
639
|
// v3.13.12: strip trailing `;` before the adapter wraps with COUNT(*)
|
|
638
640
|
// or appends LIMIT/OFFSET. Without this, `"SELECT * FROM t;"` becomes
|
|
639
641
|
// `"SELECT * FROM t; LIMIT 100 OFFSET 0"` — a syntax error.
|
|
640
642
|
sql = stripTrailingSemicolons(sql);
|
|
641
643
|
const adapter = this.getNextAdapter();
|
|
642
644
|
try {
|
|
643
|
-
|
|
645
|
+
// `opts.noCache` bypasses the query cache for this one call — no lookup,
|
|
646
|
+
// no store, run directly (mirrors the Python master's `no_cache`).
|
|
647
|
+
const rows = await adapterFetch(adapter, sql, params, limit, offset, opts?.noCache);
|
|
644
648
|
this.lastError = null;
|
|
645
649
|
return new DatabaseResult(rows, undefined, undefined, limit, offset, adapter, sql);
|
|
646
650
|
} catch (e: any) {
|
|
@@ -650,13 +654,19 @@ export class Database {
|
|
|
650
654
|
}
|
|
651
655
|
}
|
|
652
656
|
|
|
653
|
-
/**
|
|
654
|
-
|
|
657
|
+
/**
|
|
658
|
+
* Fetch a single row or null.
|
|
659
|
+
*
|
|
660
|
+
* Pass `{ noCache: true }` as the trailing options object to bypass the
|
|
661
|
+
* query cache for this one call — no lookup, no store, run directly
|
|
662
|
+
* (mirrors the Python master's `no_cache`). Default preserves caching.
|
|
663
|
+
*/
|
|
664
|
+
async fetchOne<T = Record<string, unknown>>(sql: string, params?: unknown[], opts?: { noCache?: boolean }): Promise<T | null> {
|
|
655
665
|
sql = stripTrailingSemicolons(sql);
|
|
656
666
|
const adapter = this.getNextAdapter();
|
|
657
667
|
return (adapter as any).fetchOneAsync
|
|
658
|
-
? await (adapter as any).fetchOneAsync<T>(sql, params)
|
|
659
|
-
: adapter.fetchOne<T>(sql, params);
|
|
668
|
+
? await (adapter as any).fetchOneAsync<T>(sql, params, opts?.noCache)
|
|
669
|
+
: adapter.fetchOne<T>(sql, params, opts?.noCache);
|
|
660
670
|
}
|
|
661
671
|
|
|
662
672
|
/**
|
|
@@ -671,9 +681,14 @@ export class Database {
|
|
|
671
681
|
*
|
|
672
682
|
* Returns `[]` (not `null`) when no rows match. Cross-framework parity
|
|
673
683
|
* with Python `db.fetch_all()`, PHP `$db->fetchAll()`, and Ruby `db.fetch_all`.
|
|
684
|
+
*
|
|
685
|
+
* Pass `{ noCache: true }` as the trailing options object to bypass the
|
|
686
|
+
* query cache for this one call — no lookup, no store, run directly
|
|
687
|
+
* (mirrors the Python master's `no_cache`). The options object is a
|
|
688
|
+
* SEPARATE trailing argument, never the params array.
|
|
674
689
|
*/
|
|
675
|
-
async fetchAll<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, offset?: number): Promise<T[]> {
|
|
676
|
-
return (await this.fetch(sql, params, limit, offset)).records as T[];
|
|
690
|
+
async fetchAll<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, offset?: number, opts?: { noCache?: boolean }): Promise<T[]> {
|
|
691
|
+
return (await this.fetch(sql, params, limit, offset, opts)).records as T[];
|
|
677
692
|
}
|
|
678
693
|
|
|
679
694
|
/**
|