@stacksjs/bun-router 0.0.15 → 0.0.17
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/dist/auth.d.ts +22 -8
- package/dist/chunk-1ahs68ys.js +18 -0
- package/dist/{chunk-j0e7z7hd.js → chunk-cgptvjdf.js} +153 -75
- package/dist/chunk-g3ybefhg.js +1923 -0
- package/dist/cli.js +13 -11
- package/dist/container/container.d.ts +1 -0
- package/dist/container/contextual-binding.d.ts +0 -1
- package/dist/container/decorators.d.ts +2 -5
- package/dist/container/index.d.ts +16 -0
- package/dist/container/index.js +322 -0
- package/dist/file-serving/static-files.d.ts +16 -0
- package/dist/index.d.ts +17 -1
- package/dist/index.js +1086 -1270
- package/dist/middleware/cors.d.ts +9 -0
- package/dist/middleware/csrf.d.ts +7 -0
- package/dist/middleware/ddos_protection.d.ts +16 -1
- package/dist/middleware/rate_limit.d.ts +7 -1
- package/dist/middleware/request_tracer.d.ts +15 -9
- package/dist/middleware/response_cache.d.ts +2 -1
- package/dist/request/context.d.ts +12 -1
- package/dist/request/macros.d.ts +40 -2
- package/dist/response/macros.d.ts +3 -3
- package/dist/router/fluent-routing.d.ts +50 -13
- package/dist/router/handler-resolver.d.ts +8 -0
- package/dist/router/index.d.ts +2 -0
- package/dist/router/route-compiler.d.ts +8 -10
- package/dist/router/route-trie.d.ts +7 -1
- package/dist/router/router.d.ts +13 -2
- package/dist/router/validation-integration.d.ts +10 -4
- package/dist/types/middleware-types.d.ts +1 -1
- package/dist/types/route-inference.d.ts +9 -2
- package/dist/types.d.ts +62 -9
- package/dist/utils.d.ts +1 -1
- package/package.json +11 -1
- package/src/auth.ts +32 -12
- package/src/container/container.ts +58 -12
- package/src/container/contextual-binding.ts +4 -1
- package/src/container/decorators.ts +16 -7
- package/src/container/index.ts +49 -0
- package/src/errors/circuit-breaker.ts +3 -2
- package/src/errors/graceful-degradation.ts +2 -0
- package/src/file-serving/static-files.ts +144 -7
- package/src/index.ts +53 -1
- package/src/middleware/cors.ts +76 -8
- package/src/middleware/csrf.ts +80 -22
- package/src/middleware/ddos_protection.ts +27 -3
- package/src/middleware/input_validation.ts +10 -6
- package/src/middleware/performance_alerting.ts +2 -0
- package/src/middleware/performance_monitor.ts +2 -0
- package/src/middleware/rate_limit.ts +7 -1
- package/src/middleware/request_signing.ts +2 -0
- package/src/middleware/request_tracer.ts +34 -17
- package/src/middleware/response_cache.ts +48 -13
- package/src/middleware/session.ts +3 -3
- package/src/observability/metrics.ts +1 -1
- package/src/request/context.ts +50 -3
- package/src/request/enhanced-request.ts +20 -1
- package/src/request/macros.ts +183 -49
- package/src/response/macros.ts +3 -3
- package/src/router/file-based-routing.ts +17 -10
- package/src/router/fluent-routing.ts +117 -48
- package/src/router/group-organization.ts +17 -1
- package/src/router/handler-resolver.ts +36 -0
- package/src/router/http-methods.ts +33 -3
- package/src/router/index.ts +6 -0
- package/src/router/middleware.ts +3 -0
- package/src/router/optimized-route-matching.ts +58 -9
- package/src/router/route-compiler.ts +62 -73
- package/src/router/route-matching.ts +19 -0
- package/src/router/route-trie.ts +34 -48
- package/src/router/router.ts +72 -60
- package/src/router/server.ts +361 -139
- package/src/router/validation-integration.ts +11 -5
- package/src/testing/auth-testing.ts +6 -4
- package/src/types/middleware-types.ts +7 -4
- package/src/types/route-inference.ts +29 -14
- package/src/types.ts +63 -7
- package/src/url.ts +7 -2
- package/src/utils.ts +107 -39
- package/src/validation/validator.ts +7 -4
- package/src/websocket/clustering.ts +8 -1
- package/dist/router/fluent-router.d.ts +0 -315
- package/src/router/fluent-router.ts +0 -927
package/dist/index.js
CHANGED
|
@@ -26,8 +26,6 @@ import {
|
|
|
26
26
|
Session,
|
|
27
27
|
SessionManager,
|
|
28
28
|
SignatureError,
|
|
29
|
-
__export,
|
|
30
|
-
__require,
|
|
31
29
|
apiKeyAuth,
|
|
32
30
|
auth,
|
|
33
31
|
basicAuth,
|
|
@@ -66,9 +64,50 @@ import {
|
|
|
66
64
|
signRequest,
|
|
67
65
|
singleFileUpload,
|
|
68
66
|
verifySignature
|
|
69
|
-
} from "./chunk-
|
|
67
|
+
} from "./chunk-cgptvjdf.js";
|
|
68
|
+
import {
|
|
69
|
+
AuthenticationException,
|
|
70
|
+
AuthorizationException,
|
|
71
|
+
BindingBuilder,
|
|
72
|
+
BusinessLogicException,
|
|
73
|
+
CircuitBreakerOpenException,
|
|
74
|
+
CircularDependencyException,
|
|
75
|
+
ConfigurationException,
|
|
76
|
+
Container,
|
|
77
|
+
ContextualContainer,
|
|
78
|
+
DatabaseException,
|
|
79
|
+
DefaultServiceProviderManager,
|
|
80
|
+
EnvironmentManager,
|
|
81
|
+
ErrorFactory,
|
|
82
|
+
ExternalServiceException,
|
|
83
|
+
FileSystemException,
|
|
84
|
+
HandlerResolutionException,
|
|
85
|
+
InternalServerException,
|
|
86
|
+
MethodNotAllowedException,
|
|
87
|
+
MiddlewareConfigurationException,
|
|
88
|
+
ModelBindingException,
|
|
89
|
+
NotFoundException,
|
|
90
|
+
RateLimitException,
|
|
91
|
+
RouteConflictException,
|
|
92
|
+
RouteNotFoundException,
|
|
93
|
+
RouterErrorFactory,
|
|
94
|
+
RouterException,
|
|
95
|
+
ServiceUnavailableException,
|
|
96
|
+
TimeoutException,
|
|
97
|
+
ValidationException,
|
|
98
|
+
createContainer,
|
|
99
|
+
detectRouteConflicts,
|
|
100
|
+
findSimilarRoutes,
|
|
101
|
+
getContainer,
|
|
102
|
+
setContainer
|
|
103
|
+
} from "./chunk-g3ybefhg.js";
|
|
104
|
+
import {
|
|
105
|
+
__export,
|
|
106
|
+
__require
|
|
107
|
+
} from "./chunk-1ahs68ys.js";
|
|
70
108
|
|
|
71
109
|
// src/auth.ts
|
|
110
|
+
import { timingSafeEqual } from "crypto";
|
|
72
111
|
function base64UrlEncode(str) {
|
|
73
112
|
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
74
113
|
}
|
|
@@ -132,7 +171,9 @@ class JWT {
|
|
|
132
171
|
}
|
|
133
172
|
const data = `${encodedHeader}.${encodedPayload}`;
|
|
134
173
|
const expectedSignature = this.createSignature(data, this.secret, header.alg);
|
|
135
|
-
|
|
174
|
+
const sigBuf = Buffer.from(signature);
|
|
175
|
+
const expectedBuf = Buffer.from(expectedSignature);
|
|
176
|
+
if (sigBuf.length !== expectedBuf.length || !timingSafeEqual(sigBuf, expectedBuf)) {
|
|
136
177
|
throw new Error("Invalid signature");
|
|
137
178
|
}
|
|
138
179
|
const now = Math.floor(Date.now() / 1000);
|
|
@@ -386,381 +427,6 @@ __export(exports_errors, {
|
|
|
386
427
|
AuthenticationException: () => AuthenticationException
|
|
387
428
|
});
|
|
388
429
|
|
|
389
|
-
// src/errors/exceptions.ts
|
|
390
|
-
class RouterException extends Error {
|
|
391
|
-
code;
|
|
392
|
-
statusCode;
|
|
393
|
-
context;
|
|
394
|
-
retryable;
|
|
395
|
-
severity;
|
|
396
|
-
category;
|
|
397
|
-
timestamp;
|
|
398
|
-
cause;
|
|
399
|
-
constructor(details) {
|
|
400
|
-
super(details.message);
|
|
401
|
-
this.name = this.constructor.name;
|
|
402
|
-
this.code = details.code;
|
|
403
|
-
this.statusCode = details.statusCode || 500;
|
|
404
|
-
this.context = details.context || {};
|
|
405
|
-
this.retryable = details.retryable || false;
|
|
406
|
-
this.severity = details.severity || "medium";
|
|
407
|
-
this.category = details.category || "system";
|
|
408
|
-
this.timestamp = new Date;
|
|
409
|
-
this.cause = details.cause;
|
|
410
|
-
if (Error.captureStackTrace) {
|
|
411
|
-
Error.captureStackTrace(this, this.constructor);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
toJSON() {
|
|
415
|
-
return {
|
|
416
|
-
name: this.name,
|
|
417
|
-
code: this.code,
|
|
418
|
-
message: this.message,
|
|
419
|
-
statusCode: this.statusCode,
|
|
420
|
-
context: this.context,
|
|
421
|
-
retryable: this.retryable,
|
|
422
|
-
severity: this.severity,
|
|
423
|
-
category: this.category,
|
|
424
|
-
timestamp: this.timestamp.toISOString(),
|
|
425
|
-
stack: this.stack,
|
|
426
|
-
cause: this.cause ? {
|
|
427
|
-
name: this.cause.name,
|
|
428
|
-
message: this.cause.message,
|
|
429
|
-
stack: this.cause.stack
|
|
430
|
-
} : undefined
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
toResponse() {
|
|
434
|
-
const body = {
|
|
435
|
-
error: {
|
|
436
|
-
code: this.code,
|
|
437
|
-
message: this.message,
|
|
438
|
-
statusCode: this.statusCode,
|
|
439
|
-
timestamp: this.timestamp.toISOString(),
|
|
440
|
-
requestId: this.context.requestId
|
|
441
|
-
}
|
|
442
|
-
};
|
|
443
|
-
return new Response(JSON.stringify(body), {
|
|
444
|
-
status: this.statusCode,
|
|
445
|
-
headers: {
|
|
446
|
-
"Content-Type": "application/json",
|
|
447
|
-
"X-Error-Code": this.code,
|
|
448
|
-
"X-Request-ID": this.context.requestId || ""
|
|
449
|
-
}
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
class ValidationException extends RouterException {
|
|
455
|
-
fields;
|
|
456
|
-
constructor(message, fields = {}, context) {
|
|
457
|
-
super({
|
|
458
|
-
code: "VALIDATION_ERROR",
|
|
459
|
-
message,
|
|
460
|
-
statusCode: 400,
|
|
461
|
-
context,
|
|
462
|
-
retryable: false,
|
|
463
|
-
severity: "low",
|
|
464
|
-
category: "validation"
|
|
465
|
-
});
|
|
466
|
-
this.fields = fields;
|
|
467
|
-
}
|
|
468
|
-
toResponse() {
|
|
469
|
-
const body = {
|
|
470
|
-
error: {
|
|
471
|
-
code: this.code,
|
|
472
|
-
message: this.message,
|
|
473
|
-
statusCode: this.statusCode,
|
|
474
|
-
timestamp: this.timestamp.toISOString(),
|
|
475
|
-
requestId: this.context.requestId,
|
|
476
|
-
fields: this.fields
|
|
477
|
-
}
|
|
478
|
-
};
|
|
479
|
-
return new Response(JSON.stringify(body), {
|
|
480
|
-
status: this.statusCode,
|
|
481
|
-
headers: {
|
|
482
|
-
"Content-Type": "application/json",
|
|
483
|
-
"X-Error-Code": this.code,
|
|
484
|
-
"X-Request-ID": this.context.requestId || ""
|
|
485
|
-
}
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
class AuthenticationException extends RouterException {
|
|
491
|
-
constructor(message = "Authentication required", context) {
|
|
492
|
-
super({
|
|
493
|
-
code: "AUTHENTICATION_ERROR",
|
|
494
|
-
message,
|
|
495
|
-
statusCode: 401,
|
|
496
|
-
context,
|
|
497
|
-
retryable: false,
|
|
498
|
-
severity: "medium",
|
|
499
|
-
category: "authentication"
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
class AuthorizationException extends RouterException {
|
|
505
|
-
requiredPermissions;
|
|
506
|
-
constructor(message = "Access denied", requiredPermissions, context) {
|
|
507
|
-
super({
|
|
508
|
-
code: "AUTHORIZATION_ERROR",
|
|
509
|
-
message,
|
|
510
|
-
statusCode: 403,
|
|
511
|
-
context,
|
|
512
|
-
retryable: false,
|
|
513
|
-
severity: "medium",
|
|
514
|
-
category: "authorization"
|
|
515
|
-
});
|
|
516
|
-
this.requiredPermissions = requiredPermissions;
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
class NotFoundException extends RouterException {
|
|
521
|
-
resource;
|
|
522
|
-
resourceId;
|
|
523
|
-
constructor(message = "Resource not found", resource, resourceId, context) {
|
|
524
|
-
super({
|
|
525
|
-
code: "NOT_FOUND",
|
|
526
|
-
message,
|
|
527
|
-
statusCode: 404,
|
|
528
|
-
context,
|
|
529
|
-
retryable: false,
|
|
530
|
-
severity: "low",
|
|
531
|
-
category: "business"
|
|
532
|
-
});
|
|
533
|
-
this.resource = resource;
|
|
534
|
-
this.resourceId = resourceId;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
class MethodNotAllowedException extends RouterException {
|
|
539
|
-
allowedMethods;
|
|
540
|
-
constructor(method, allowedMethods, context) {
|
|
541
|
-
super({
|
|
542
|
-
code: "METHOD_NOT_ALLOWED",
|
|
543
|
-
message: `Method ${method} not allowed. Allowed methods: ${allowedMethods.join(", ")}`,
|
|
544
|
-
statusCode: 405,
|
|
545
|
-
context,
|
|
546
|
-
retryable: false,
|
|
547
|
-
severity: "low",
|
|
548
|
-
category: "business"
|
|
549
|
-
});
|
|
550
|
-
this.allowedMethods = allowedMethods;
|
|
551
|
-
}
|
|
552
|
-
toResponse() {
|
|
553
|
-
const response = super.toResponse();
|
|
554
|
-
response.headers.set("Allow", this.allowedMethods.join(", "));
|
|
555
|
-
return response;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
class TimeoutException extends RouterException {
|
|
560
|
-
timeoutMs;
|
|
561
|
-
constructor(message = "Request timeout", timeoutMs, context) {
|
|
562
|
-
super({
|
|
563
|
-
code: "REQUEST_TIMEOUT",
|
|
564
|
-
message,
|
|
565
|
-
statusCode: 408,
|
|
566
|
-
context,
|
|
567
|
-
retryable: true,
|
|
568
|
-
severity: "medium",
|
|
569
|
-
category: "network"
|
|
570
|
-
});
|
|
571
|
-
this.timeoutMs = timeoutMs;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
class RateLimitException extends RouterException {
|
|
576
|
-
retryAfter;
|
|
577
|
-
limit;
|
|
578
|
-
remaining;
|
|
579
|
-
constructor(message = "Rate limit exceeded", retryAfter, limit, remaining = 0, context) {
|
|
580
|
-
super({
|
|
581
|
-
code: "RATE_LIMIT_EXCEEDED",
|
|
582
|
-
message,
|
|
583
|
-
statusCode: 429,
|
|
584
|
-
context,
|
|
585
|
-
retryable: true,
|
|
586
|
-
severity: "medium",
|
|
587
|
-
category: "network"
|
|
588
|
-
});
|
|
589
|
-
this.retryAfter = retryAfter;
|
|
590
|
-
this.limit = limit;
|
|
591
|
-
this.remaining = remaining;
|
|
592
|
-
}
|
|
593
|
-
toResponse() {
|
|
594
|
-
const response = super.toResponse();
|
|
595
|
-
response.headers.set("Retry-After", this.retryAfter.toString());
|
|
596
|
-
response.headers.set("X-RateLimit-Limit", this.limit.toString());
|
|
597
|
-
response.headers.set("X-RateLimit-Remaining", this.remaining.toString());
|
|
598
|
-
return response;
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
class InternalServerException extends RouterException {
|
|
603
|
-
constructor(message = "Internal server error", cause, context) {
|
|
604
|
-
super({
|
|
605
|
-
code: "INTERNAL_SERVER_ERROR",
|
|
606
|
-
message,
|
|
607
|
-
statusCode: 500,
|
|
608
|
-
context,
|
|
609
|
-
cause,
|
|
610
|
-
retryable: true,
|
|
611
|
-
severity: "high",
|
|
612
|
-
category: "system"
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
class ServiceUnavailableException extends RouterException {
|
|
618
|
-
retryAfter;
|
|
619
|
-
constructor(message = "Service unavailable", retryAfter, context) {
|
|
620
|
-
super({
|
|
621
|
-
code: "SERVICE_UNAVAILABLE",
|
|
622
|
-
message,
|
|
623
|
-
statusCode: 503,
|
|
624
|
-
context,
|
|
625
|
-
retryable: true,
|
|
626
|
-
severity: "high",
|
|
627
|
-
category: "system"
|
|
628
|
-
});
|
|
629
|
-
this.retryAfter = retryAfter;
|
|
630
|
-
}
|
|
631
|
-
toResponse() {
|
|
632
|
-
const response = super.toResponse();
|
|
633
|
-
if (this.retryAfter) {
|
|
634
|
-
response.headers.set("Retry-After", this.retryAfter.toString());
|
|
635
|
-
}
|
|
636
|
-
return response;
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
class DatabaseException extends RouterException {
|
|
641
|
-
query;
|
|
642
|
-
operation;
|
|
643
|
-
constructor(message, operation, query, cause, context) {
|
|
644
|
-
super({
|
|
645
|
-
code: "DATABASE_ERROR",
|
|
646
|
-
message,
|
|
647
|
-
statusCode: 500,
|
|
648
|
-
context,
|
|
649
|
-
cause,
|
|
650
|
-
retryable: true,
|
|
651
|
-
severity: "high",
|
|
652
|
-
category: "database"
|
|
653
|
-
});
|
|
654
|
-
this.operation = operation;
|
|
655
|
-
this.query = query;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
class ExternalServiceException extends RouterException {
|
|
660
|
-
service;
|
|
661
|
-
endpoint;
|
|
662
|
-
constructor(message, service, endpoint, statusCode = 502, cause, context) {
|
|
663
|
-
super({
|
|
664
|
-
code: "EXTERNAL_SERVICE_ERROR",
|
|
665
|
-
message,
|
|
666
|
-
statusCode,
|
|
667
|
-
context,
|
|
668
|
-
cause,
|
|
669
|
-
retryable: true,
|
|
670
|
-
severity: "medium",
|
|
671
|
-
category: "external"
|
|
672
|
-
});
|
|
673
|
-
this.service = service;
|
|
674
|
-
this.endpoint = endpoint;
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
class CircuitBreakerOpenException extends RouterException {
|
|
679
|
-
service;
|
|
680
|
-
nextAttemptAt;
|
|
681
|
-
constructor(service, nextAttemptAt, context) {
|
|
682
|
-
super({
|
|
683
|
-
code: "CIRCUIT_BREAKER_OPEN",
|
|
684
|
-
message: `Circuit breaker is open for service: ${service}. Next attempt at: ${nextAttemptAt.toISOString()}`,
|
|
685
|
-
statusCode: 503,
|
|
686
|
-
context,
|
|
687
|
-
retryable: true,
|
|
688
|
-
severity: "medium",
|
|
689
|
-
category: "external"
|
|
690
|
-
});
|
|
691
|
-
this.service = service;
|
|
692
|
-
this.nextAttemptAt = nextAttemptAt;
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
class BusinessLogicException extends RouterException {
|
|
697
|
-
businessRule;
|
|
698
|
-
constructor(message, businessRule, context) {
|
|
699
|
-
super({
|
|
700
|
-
code: "BUSINESS_LOGIC_ERROR",
|
|
701
|
-
message,
|
|
702
|
-
statusCode: 422,
|
|
703
|
-
context,
|
|
704
|
-
retryable: false,
|
|
705
|
-
severity: "low",
|
|
706
|
-
category: "business"
|
|
707
|
-
});
|
|
708
|
-
this.businessRule = businessRule;
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
class ConfigurationException extends RouterException {
|
|
713
|
-
configKey;
|
|
714
|
-
constructor(message, configKey, context) {
|
|
715
|
-
super({
|
|
716
|
-
code: "CONFIGURATION_ERROR",
|
|
717
|
-
message,
|
|
718
|
-
statusCode: 500,
|
|
719
|
-
context,
|
|
720
|
-
retryable: false,
|
|
721
|
-
severity: "critical",
|
|
722
|
-
category: "system"
|
|
723
|
-
});
|
|
724
|
-
this.configKey = configKey;
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
class FileSystemException extends RouterException {
|
|
729
|
-
path;
|
|
730
|
-
operation;
|
|
731
|
-
constructor(message, path, operation, cause, context) {
|
|
732
|
-
super({
|
|
733
|
-
code: "FILESYSTEM_ERROR",
|
|
734
|
-
message,
|
|
735
|
-
statusCode: 500,
|
|
736
|
-
context,
|
|
737
|
-
cause,
|
|
738
|
-
retryable: false,
|
|
739
|
-
severity: "medium",
|
|
740
|
-
category: "system"
|
|
741
|
-
});
|
|
742
|
-
this.path = path;
|
|
743
|
-
this.operation = operation;
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
var ErrorFactory = {
|
|
747
|
-
validation: (message, fields, context) => new ValidationException(message, fields, context),
|
|
748
|
-
authentication: (message, context) => new AuthenticationException(message, context),
|
|
749
|
-
authorization: (message, requiredPermissions, context) => new AuthorizationException(message, requiredPermissions, context),
|
|
750
|
-
notFound: (resource, resourceId, context) => new NotFoundException(undefined, resource, resourceId, context),
|
|
751
|
-
methodNotAllowed: (method, allowedMethods, context) => new MethodNotAllowedException(method, allowedMethods, context),
|
|
752
|
-
timeout: (timeoutMs, message, context) => new TimeoutException(message, timeoutMs, context),
|
|
753
|
-
rateLimit: (retryAfter, limit, remaining, context) => new RateLimitException(undefined, retryAfter, limit, remaining, context),
|
|
754
|
-
internalServer: (message, cause, context) => new InternalServerException(message, cause, context),
|
|
755
|
-
serviceUnavailable: (message, retryAfter, context) => new ServiceUnavailableException(message, retryAfter, context),
|
|
756
|
-
database: (message, operation, query, cause, context) => new DatabaseException(message, operation, query, cause, context),
|
|
757
|
-
externalService: (message, service, endpoint, statusCode, cause, context) => new ExternalServiceException(message, service, endpoint, statusCode, cause, context),
|
|
758
|
-
circuitBreakerOpen: (service, nextAttemptAt, context) => new CircuitBreakerOpenException(service, nextAttemptAt, context),
|
|
759
|
-
businessLogic: (message, businessRule, context) => new BusinessLogicException(message, businessRule, context),
|
|
760
|
-
configuration: (message, configKey, context) => new ConfigurationException(message, configKey, context),
|
|
761
|
-
fileSystem: (message, path, operation, cause, context) => new FileSystemException(message, path, operation, cause, context)
|
|
762
|
-
};
|
|
763
|
-
|
|
764
430
|
// src/errors/circuit-breaker.ts
|
|
765
431
|
class CircuitBreaker {
|
|
766
432
|
config;
|
|
@@ -794,7 +460,7 @@ class CircuitBreaker {
|
|
|
794
460
|
consecutiveFailures: 0,
|
|
795
461
|
consecutiveSuccesses: 0
|
|
796
462
|
};
|
|
797
|
-
setInterval(() => this.cleanupOldRequests(), this.config.monitoringPeriod / 4);
|
|
463
|
+
setInterval(() => this.cleanupOldRequests(), this.config.monitoringPeriod / 4).unref?.();
|
|
798
464
|
}
|
|
799
465
|
async execute(call) {
|
|
800
466
|
if (this.state === "OPEN") {
|
|
@@ -1705,6 +1371,7 @@ class ServiceHealthMonitor {
|
|
|
1705
1371
|
consecutiveFailures: 0
|
|
1706
1372
|
});
|
|
1707
1373
|
const interval = setInterval(() => this.performHealthCheck(serviceName, healthConfig), healthConfig.interval);
|
|
1374
|
+
interval.unref?.();
|
|
1708
1375
|
this.healthCheckIntervals.set(serviceName, interval);
|
|
1709
1376
|
}
|
|
1710
1377
|
}
|
|
@@ -2334,448 +2001,44 @@ function createAdvancedErrorHandler(config2 = {}) {
|
|
|
2334
2001
|
cleanup: () => handler.cleanup()
|
|
2335
2002
|
};
|
|
2336
2003
|
}
|
|
2337
|
-
// src/errors/router-errors.ts
|
|
2338
|
-
class RouteConflictException extends RouterException {
|
|
2339
|
-
conflictingRoutes;
|
|
2340
|
-
suggestion;
|
|
2341
|
-
constructor(conflictingRoutes, context) {
|
|
2342
|
-
const message = RouteConflictException.buildMessage(conflictingRoutes);
|
|
2343
|
-
const suggestion = RouteConflictException.buildSuggestion(conflictingRoutes);
|
|
2344
|
-
super({
|
|
2345
|
-
code: "ROUTE_CONFLICT",
|
|
2346
|
-
message,
|
|
2347
|
-
statusCode: 500,
|
|
2348
|
-
context,
|
|
2349
|
-
retryable: false,
|
|
2350
|
-
severity: "critical",
|
|
2351
|
-
category: "system"
|
|
2352
|
-
});
|
|
2353
|
-
this.conflictingRoutes = conflictingRoutes;
|
|
2354
|
-
this.suggestion = suggestion;
|
|
2355
|
-
}
|
|
2356
|
-
static buildMessage(routes) {
|
|
2357
|
-
const lines = [
|
|
2358
|
-
"Route conflict detected! Multiple routes match the same path pattern.",
|
|
2359
|
-
"",
|
|
2360
|
-
"Conflicting routes:"
|
|
2361
|
-
];
|
|
2362
|
-
for (const route of routes) {
|
|
2363
|
-
lines.push(` \u2022 ${route.method} ${route.path}`);
|
|
2364
|
-
if (route.name) {
|
|
2365
|
-
lines.push(` Name: "${route.name}"`);
|
|
2366
|
-
}
|
|
2367
|
-
if (route.definedAt) {
|
|
2368
|
-
lines.push(` Defined at: ${route.definedAt}`);
|
|
2369
|
-
}
|
|
2370
|
-
}
|
|
2371
|
-
return lines.join(`
|
|
2372
|
-
`);
|
|
2373
|
-
}
|
|
2374
|
-
static buildSuggestion(routes) {
|
|
2375
|
-
const suggestions = [];
|
|
2376
|
-
const hasStatic = routes.some((r) => !r.path.includes("{"));
|
|
2377
|
-
const hasDynamic = routes.some((r) => r.path.includes("{"));
|
|
2378
|
-
if (hasStatic && hasDynamic) {
|
|
2379
|
-
suggestions.push("\u2022 Static routes should be defined before dynamic routes", "\u2022 Consider using route constraints (whereNumber, whereUuid) to differentiate");
|
|
2380
|
-
}
|
|
2381
|
-
const dynamicRoutes = routes.filter((r) => r.path.includes("{"));
|
|
2382
|
-
if (dynamicRoutes.length > 1) {
|
|
2383
|
-
suggestions.push("\u2022 Use different parameter names or add constraints:", ' router.get("/users/{id}").whereNumber("id")', ' router.get("/users/{slug}").whereAlpha("slug")');
|
|
2384
|
-
}
|
|
2385
|
-
const hasWildcard = routes.some((r) => r.path.includes("*"));
|
|
2386
|
-
if (hasWildcard) {
|
|
2387
|
-
suggestions.push("\u2022 Wildcard routes should be defined last", "\u2022 Consider using more specific path prefixes");
|
|
2388
|
-
}
|
|
2389
|
-
if (suggestions.length === 0) {
|
|
2390
|
-
suggestions.push("\u2022 Rename one of the conflicting routes", "\u2022 Use route groups with different prefixes", "\u2022 Add parameter constraints to differentiate routes");
|
|
2391
|
-
}
|
|
2392
|
-
return `How to fix:
|
|
2393
|
-
${suggestions.join(`
|
|
2394
|
-
`)}`;
|
|
2395
|
-
}
|
|
2396
|
-
toJSON() {
|
|
2397
|
-
return {
|
|
2398
|
-
...super.toJSON(),
|
|
2399
|
-
conflictingRoutes: this.conflictingRoutes,
|
|
2400
|
-
suggestion: this.suggestion
|
|
2401
|
-
};
|
|
2402
|
-
}
|
|
2403
|
-
}
|
|
2404
|
-
|
|
2405
|
-
class CircularDependencyException extends RouterException {
|
|
2406
|
-
dependencyChain;
|
|
2407
|
-
suggestion;
|
|
2408
|
-
constructor(dependencyChain, context) {
|
|
2409
|
-
const message = CircularDependencyException.buildMessage(dependencyChain);
|
|
2410
|
-
const suggestion = CircularDependencyException.buildSuggestion(dependencyChain);
|
|
2411
|
-
super({
|
|
2412
|
-
code: "CIRCULAR_DEPENDENCY",
|
|
2413
|
-
message,
|
|
2414
|
-
statusCode: 500,
|
|
2415
|
-
context,
|
|
2416
|
-
retryable: false,
|
|
2417
|
-
severity: "critical",
|
|
2418
|
-
category: "system"
|
|
2419
|
-
});
|
|
2420
|
-
this.dependencyChain = dependencyChain;
|
|
2421
|
-
this.suggestion = suggestion;
|
|
2422
|
-
}
|
|
2423
|
-
static buildMessage(chain) {
|
|
2424
|
-
const lines = [
|
|
2425
|
-
"Circular dependency detected in the dependency injection container!",
|
|
2426
|
-
"",
|
|
2427
|
-
"Dependency chain:",
|
|
2428
|
-
""
|
|
2429
|
-
];
|
|
2430
|
-
for (let i = 0;i < chain.length; i++) {
|
|
2431
|
-
const isLast = i === chain.length - 1;
|
|
2432
|
-
const prefix = i === 0 ? "\u250C\u2500\u25BA" : isLast ? "\u2514\u2500\u25BA" : "\u251C\u2500\u25BA";
|
|
2433
|
-
lines.push(` ${prefix} ${chain[i]}`);
|
|
2434
|
-
if (!isLast) {
|
|
2435
|
-
lines.push(" \u2502");
|
|
2436
|
-
}
|
|
2437
|
-
}
|
|
2438
|
-
lines.push(" \u2502");
|
|
2439
|
-
lines.push(` \u2514\u2500\u2500 cycles back to: ${chain[0]}`);
|
|
2440
|
-
return lines.join(`
|
|
2441
|
-
`);
|
|
2442
|
-
}
|
|
2443
|
-
static buildSuggestion(chain) {
|
|
2444
|
-
return `How to fix:
|
|
2445
|
-
|
|
2446
|
-
1. Break the cycle by introducing an interface:
|
|
2447
|
-
\u2022 Create an interface for one of the dependencies
|
|
2448
|
-
\u2022 Use lazy injection: container.bind('${chain[0]}').lazy()
|
|
2449
|
-
|
|
2450
|
-
2. Restructure your dependencies:
|
|
2451
|
-
\u2022 Extract shared logic into a separate service
|
|
2452
|
-
\u2022 Use events/callbacks instead of direct dependencies
|
|
2453
|
-
|
|
2454
|
-
3. Use setter injection instead of constructor injection:
|
|
2455
|
-
\u2022 Remove the dependency from the constructor
|
|
2456
|
-
\u2022 Set it after instantiation using a setter method
|
|
2457
|
-
|
|
2458
|
-
4. Check if the dependency is actually needed:
|
|
2459
|
-
\u2022 Sometimes circular dependencies indicate a design issue
|
|
2460
|
-
\u2022 Consider if the classes have too many responsibilities
|
|
2461
|
-
|
|
2462
|
-
Dependency chain involved:
|
|
2463
|
-
${chain.map((dep, i) => ` ${i + 1}. ${dep}`).join(`
|
|
2464
|
-
`)}`;
|
|
2465
|
-
}
|
|
2466
|
-
toJSON() {
|
|
2467
|
-
return {
|
|
2468
|
-
...super.toJSON(),
|
|
2469
|
-
dependencyChain: this.dependencyChain,
|
|
2470
|
-
suggestion: this.suggestion
|
|
2471
|
-
};
|
|
2472
|
-
}
|
|
2473
|
-
}
|
|
2474
|
-
|
|
2475
|
-
class MiddlewareConfigurationException extends RouterException {
|
|
2476
|
-
middlewareName;
|
|
2477
|
-
configErrors;
|
|
2478
|
-
suggestion;
|
|
2479
|
-
constructor(middlewareName, configErrors, context) {
|
|
2480
|
-
const message = `Middleware "${middlewareName}" configuration error:
|
|
2481
|
-
${configErrors.map((e) => ` \u2022 ${e}`).join(`
|
|
2482
|
-
`)}`;
|
|
2483
|
-
super({
|
|
2484
|
-
code: "MIDDLEWARE_CONFIGURATION_ERROR",
|
|
2485
|
-
message,
|
|
2486
|
-
statusCode: 500,
|
|
2487
|
-
context,
|
|
2488
|
-
retryable: false,
|
|
2489
|
-
severity: "high",
|
|
2490
|
-
category: "system"
|
|
2491
|
-
});
|
|
2492
|
-
this.middlewareName = middlewareName;
|
|
2493
|
-
this.configErrors = configErrors;
|
|
2494
|
-
this.suggestion = this.buildSuggestion();
|
|
2495
|
-
}
|
|
2496
|
-
buildSuggestion() {
|
|
2497
|
-
return `How to fix:
|
|
2498
|
-
\u2022 Check the middleware documentation for required options
|
|
2499
|
-
\u2022 Ensure all required configuration values are provided
|
|
2500
|
-
\u2022 Verify configuration value types match expected types
|
|
2501
|
-
|
|
2502
|
-
Example configuration:
|
|
2503
|
-
router.use(new ${this.middlewareName}({
|
|
2504
|
-
// Add required options here
|
|
2505
|
-
}))`;
|
|
2506
|
-
}
|
|
2507
|
-
}
|
|
2508
|
-
|
|
2509
|
-
class RouteNotFoundException extends RouterException {
|
|
2510
|
-
requestedPath;
|
|
2511
|
-
requestedMethod;
|
|
2512
|
-
similarRoutes;
|
|
2513
|
-
suggestion;
|
|
2514
|
-
constructor(requestedMethod, requestedPath, similarRoutes = [], context) {
|
|
2515
|
-
const message = RouteNotFoundException.buildMessage(requestedMethod, requestedPath, similarRoutes);
|
|
2516
|
-
super({
|
|
2517
|
-
code: "ROUTE_NOT_FOUND",
|
|
2518
|
-
message,
|
|
2519
|
-
statusCode: 404,
|
|
2520
|
-
context,
|
|
2521
|
-
retryable: false,
|
|
2522
|
-
severity: "low",
|
|
2523
|
-
category: "business"
|
|
2524
|
-
});
|
|
2525
|
-
this.requestedPath = requestedPath;
|
|
2526
|
-
this.requestedMethod = requestedMethod;
|
|
2527
|
-
this.similarRoutes = similarRoutes;
|
|
2528
|
-
this.suggestion = this.buildSuggestion();
|
|
2529
|
-
}
|
|
2530
|
-
static buildMessage(method, path, similarRoutes) {
|
|
2531
|
-
const lines = [
|
|
2532
|
-
`Route not found: ${method} ${path}`
|
|
2533
|
-
];
|
|
2534
|
-
if (similarRoutes.length > 0) {
|
|
2535
|
-
lines.push("");
|
|
2536
|
-
lines.push("Did you mean one of these routes?");
|
|
2537
|
-
for (const route of similarRoutes.slice(0, 5)) {
|
|
2538
|
-
lines.push(` \u2022 ${route.method} ${route.path}`);
|
|
2539
|
-
if (route.similarity) {
|
|
2540
|
-
lines.push(` (${Math.round(route.similarity * 100)}% match)`);
|
|
2541
|
-
}
|
|
2542
|
-
}
|
|
2543
|
-
}
|
|
2544
|
-
return lines.join(`
|
|
2545
|
-
`);
|
|
2546
|
-
}
|
|
2547
|
-
buildSuggestion() {
|
|
2548
|
-
const suggestions = [
|
|
2549
|
-
`\u2022 Verify the HTTP method is correct (you used ${this.requestedMethod})`,
|
|
2550
|
-
"\u2022 Check for typos in the URL path",
|
|
2551
|
-
"\u2022 Ensure the route is registered before the server starts",
|
|
2552
|
-
"\u2022 Check if the route requires specific middleware (e.g., auth)"
|
|
2553
|
-
];
|
|
2554
|
-
if (this.requestedPath.includes("//")) {
|
|
2555
|
-
suggestions.push("\u2022 Remove duplicate slashes from the URL");
|
|
2556
|
-
}
|
|
2557
|
-
if (!this.requestedPath.startsWith("/")) {
|
|
2558
|
-
suggestions.push("\u2022 Ensure the path starts with a forward slash (/)");
|
|
2559
|
-
}
|
|
2560
|
-
return `How to fix:
|
|
2561
|
-
${suggestions.join(`
|
|
2562
|
-
`)}`;
|
|
2563
|
-
}
|
|
2564
|
-
}
|
|
2565
|
-
|
|
2566
|
-
class HandlerResolutionException extends RouterException {
|
|
2567
|
-
handlerRef;
|
|
2568
|
-
suggestion;
|
|
2569
|
-
constructor(handlerRef, reason, context) {
|
|
2570
|
-
const message = `Failed to resolve handler "${handlerRef}": ${reason}`;
|
|
2571
|
-
super({
|
|
2572
|
-
code: "HANDLER_RESOLUTION_ERROR",
|
|
2573
|
-
message,
|
|
2574
|
-
statusCode: 500,
|
|
2575
|
-
context,
|
|
2576
|
-
retryable: false,
|
|
2577
|
-
severity: "high",
|
|
2578
|
-
category: "system"
|
|
2579
|
-
});
|
|
2580
|
-
this.handlerRef = handlerRef;
|
|
2581
|
-
this.suggestion = this.buildSuggestion(handlerRef);
|
|
2582
|
-
}
|
|
2583
|
-
buildSuggestion(handlerRef) {
|
|
2584
|
-
const isControllerMethod = handlerRef.includes("@");
|
|
2585
|
-
const isActionPath = handlerRef.includes("/");
|
|
2586
|
-
if (isControllerMethod) {
|
|
2587
|
-
const [controller, method] = handlerRef.split("@");
|
|
2588
|
-
return `How to fix:
|
|
2589
|
-
\u2022 Ensure the controller "${controller}" is registered in the container
|
|
2590
|
-
\u2022 Verify the method "${method}" exists and is public
|
|
2591
|
-
\u2022 Check the controller file is imported
|
|
2592
|
-
|
|
2593
|
-
Example:
|
|
2594
|
-
// Register the controller
|
|
2595
|
-
container.singleton('${controller}', ${controller})
|
|
2596
|
-
|
|
2597
|
-
// Define the route
|
|
2598
|
-
router.get('/path', '${handlerRef}')`;
|
|
2599
|
-
}
|
|
2600
|
-
if (isActionPath) {
|
|
2601
|
-
return `How to fix:
|
|
2602
|
-
\u2022 Ensure the action class exists at the specified path
|
|
2603
|
-
\u2022 Verify the action has a "handle" method
|
|
2604
|
-
\u2022 Check the action file is properly exported
|
|
2605
|
-
|
|
2606
|
-
Example:
|
|
2607
|
-
// Action class at ${handlerRef}.ts
|
|
2608
|
-
export default class ${handlerRef.split("/").pop()} {
|
|
2609
|
-
handle(req: Request): Response {
|
|
2610
|
-
return new Response('OK')
|
|
2611
|
-
}
|
|
2612
|
-
}`;
|
|
2613
|
-
}
|
|
2614
|
-
return `How to fix:
|
|
2615
|
-
\u2022 Verify the handler reference is correct
|
|
2616
|
-
\u2022 Ensure the handler function/class is exported
|
|
2617
|
-
\u2022 Check for circular dependencies in the handler module`;
|
|
2618
|
-
}
|
|
2619
|
-
}
|
|
2620
|
-
|
|
2621
|
-
class ModelBindingException extends RouterException {
|
|
2622
|
-
modelName;
|
|
2623
|
-
paramName;
|
|
2624
|
-
paramValue;
|
|
2625
|
-
suggestion;
|
|
2626
|
-
constructor(modelName, paramName, paramValue, reason, context) {
|
|
2627
|
-
const message = `Failed to bind model "${modelName}" for parameter "${paramName}" with value "${paramValue}": ${reason}`;
|
|
2628
|
-
super({
|
|
2629
|
-
code: "MODEL_BINDING_ERROR",
|
|
2630
|
-
message,
|
|
2631
|
-
statusCode: 404,
|
|
2632
|
-
context,
|
|
2633
|
-
retryable: false,
|
|
2634
|
-
severity: "low",
|
|
2635
|
-
category: "business"
|
|
2636
|
-
});
|
|
2637
|
-
this.modelName = modelName;
|
|
2638
|
-
this.paramName = paramName;
|
|
2639
|
-
this.paramValue = paramValue;
|
|
2640
|
-
this.suggestion = this.buildSuggestion();
|
|
2641
|
-
}
|
|
2642
|
-
buildSuggestion() {
|
|
2643
|
-
return `How to fix:
|
|
2644
|
-
\u2022 Verify the ${this.paramName} "${this.paramValue}" exists in your data source
|
|
2645
|
-
\u2022 Check if the model resolver is correctly configured
|
|
2646
|
-
\u2022 Ensure the route parameter matches the model's identifier field
|
|
2647
|
-
|
|
2648
|
-
Example model binding:
|
|
2649
|
-
router.model('${this.paramName}', async (value) => {
|
|
2650
|
-
const ${this.modelName.toLowerCase()} = await db.${this.modelName.toLowerCase()}s.findUnique({
|
|
2651
|
-
where: { id: value }
|
|
2652
|
-
})
|
|
2653
|
-
if (!${this.modelName.toLowerCase()}) {
|
|
2654
|
-
throw new Error('${this.modelName} not found')
|
|
2655
|
-
}
|
|
2656
|
-
return ${this.modelName.toLowerCase()}
|
|
2657
|
-
})`;
|
|
2658
|
-
}
|
|
2659
|
-
}
|
|
2660
|
-
function findSimilarRoutes(requestedPath, availableRoutes, maxResults = 5) {
|
|
2661
|
-
const results = [];
|
|
2662
|
-
for (const route of availableRoutes) {
|
|
2663
|
-
const similarity = calculateSimilarity(requestedPath, route.path);
|
|
2664
|
-
if (similarity > 0.3) {
|
|
2665
|
-
results.push({
|
|
2666
|
-
method: route.method,
|
|
2667
|
-
path: route.path,
|
|
2668
|
-
similarity
|
|
2669
|
-
});
|
|
2670
|
-
}
|
|
2671
|
-
}
|
|
2672
|
-
return results.sort((a, b) => (b.similarity || 0) - (a.similarity || 0)).slice(0, maxResults);
|
|
2673
|
-
}
|
|
2674
|
-
function calculateSimilarity(str1, str2) {
|
|
2675
|
-
const len1 = str1.length;
|
|
2676
|
-
const len2 = str2.length;
|
|
2677
|
-
if (len1 === 0)
|
|
2678
|
-
return len2 === 0 ? 1 : 0;
|
|
2679
|
-
if (len2 === 0)
|
|
2680
|
-
return 0;
|
|
2681
|
-
const matrix = [];
|
|
2682
|
-
for (let i = 0;i <= len1; i++) {
|
|
2683
|
-
matrix[i] = [i];
|
|
2684
|
-
}
|
|
2685
|
-
for (let j = 0;j <= len2; j++) {
|
|
2686
|
-
matrix[0][j] = j;
|
|
2687
|
-
}
|
|
2688
|
-
for (let i = 1;i <= len1; i++) {
|
|
2689
|
-
for (let j = 1;j <= len2; j++) {
|
|
2690
|
-
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
|
|
2691
|
-
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
|
|
2692
|
-
}
|
|
2693
|
-
}
|
|
2694
|
-
const distance = matrix[len1][len2];
|
|
2695
|
-
const maxLen = Math.max(len1, len2);
|
|
2696
|
-
return 1 - distance / maxLen;
|
|
2697
|
-
}
|
|
2698
|
-
function detectRouteConflicts(routes) {
|
|
2699
|
-
const conflicts = [];
|
|
2700
|
-
const checked = new Set;
|
|
2701
|
-
for (let i = 0;i < routes.length; i++) {
|
|
2702
|
-
const route1 = routes[i];
|
|
2703
|
-
const key1 = `${route1.method}:${route1.path}`;
|
|
2704
|
-
if (checked.has(key1))
|
|
2705
|
-
continue;
|
|
2706
|
-
const conflicting = [];
|
|
2707
|
-
for (let j = i + 1;j < routes.length; j++) {
|
|
2708
|
-
const route2 = routes[j];
|
|
2709
|
-
if (route1.method === route2.method && routesConflict(route1.path, route2.path)) {
|
|
2710
|
-
if (conflicting.length === 0) {
|
|
2711
|
-
conflicting.push({
|
|
2712
|
-
method: route1.method,
|
|
2713
|
-
path: route1.path,
|
|
2714
|
-
name: route1.name
|
|
2715
|
-
});
|
|
2716
|
-
}
|
|
2717
|
-
conflicting.push({
|
|
2718
|
-
method: route2.method,
|
|
2719
|
-
path: route2.path,
|
|
2720
|
-
name: route2.name
|
|
2721
|
-
});
|
|
2722
|
-
checked.add(`${route2.method}:${route2.path}`);
|
|
2723
|
-
}
|
|
2724
|
-
}
|
|
2725
|
-
if (conflicting.length > 0) {
|
|
2726
|
-
conflicts.push(conflicting);
|
|
2727
|
-
}
|
|
2728
|
-
checked.add(key1);
|
|
2729
|
-
}
|
|
2730
|
-
return conflicts;
|
|
2731
|
-
}
|
|
2732
|
-
function routesConflict(path1, path2) {
|
|
2733
|
-
if (path1 === path2)
|
|
2734
|
-
return true;
|
|
2735
|
-
const segments1 = path1.split("/").filter(Boolean);
|
|
2736
|
-
const segments2 = path2.split("/").filter(Boolean);
|
|
2737
|
-
if (segments1.length !== segments2.length) {
|
|
2738
|
-
const hasWildcard1 = path1.includes("*");
|
|
2739
|
-
const hasWildcard2 = path2.includes("*");
|
|
2740
|
-
if (!hasWildcard1 && !hasWildcard2)
|
|
2741
|
-
return false;
|
|
2742
|
-
}
|
|
2743
|
-
const maxLen = Math.max(segments1.length, segments2.length);
|
|
2744
|
-
for (let i = 0;i < maxLen; i++) {
|
|
2745
|
-
const seg1 = segments1[i] || "";
|
|
2746
|
-
const seg2 = segments2[i] || "";
|
|
2747
|
-
if (seg1 === "*" || seg2 === "*")
|
|
2748
|
-
return true;
|
|
2749
|
-
const isDynamic1 = seg1.startsWith("{") && seg1.endsWith("}");
|
|
2750
|
-
const isDynamic2 = seg2.startsWith("{") && seg2.endsWith("}");
|
|
2751
|
-
if (!isDynamic1 && !isDynamic2) {
|
|
2752
|
-
if (seg1 !== seg2)
|
|
2753
|
-
return false;
|
|
2754
|
-
}
|
|
2755
|
-
}
|
|
2756
|
-
return true;
|
|
2757
|
-
}
|
|
2758
|
-
var RouterErrorFactory = {
|
|
2759
|
-
routeConflict: (routes, context) => new RouteConflictException(routes, context),
|
|
2760
|
-
circularDependency: (chain, context) => new CircularDependencyException(chain, context),
|
|
2761
|
-
middlewareConfig: (name, errors, context) => new MiddlewareConfigurationException(name, errors, context),
|
|
2762
|
-
routeNotFound: (method, path, similarRoutes, context) => new RouteNotFoundException(method, path, similarRoutes, context),
|
|
2763
|
-
handlerResolution: (ref, reason, context) => new HandlerResolutionException(ref, reason, context),
|
|
2764
|
-
modelBinding: (model, param, value, reason, context) => new ModelBindingException(model, param, value, reason, context)
|
|
2765
|
-
};
|
|
2766
2004
|
// src/request/context.ts
|
|
2767
2005
|
import { AsyncLocalStorage } from "async_hooks";
|
|
2768
2006
|
var storage = new AsyncLocalStorage;
|
|
2007
|
+
var contextEnabled = false;
|
|
2008
|
+
var syncCurrent = null;
|
|
2009
|
+
function enableRequestContext() {
|
|
2010
|
+
contextEnabled = true;
|
|
2011
|
+
}
|
|
2769
2012
|
function runWithRequest(initial, fn) {
|
|
2013
|
+
if (!contextEnabled) {
|
|
2014
|
+
syncCurrent = initial;
|
|
2015
|
+
const result = fn();
|
|
2016
|
+
if (result instanceof Promise) {
|
|
2017
|
+
return result.finally(() => {
|
|
2018
|
+
syncCurrent = null;
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
syncCurrent = null;
|
|
2022
|
+
return result;
|
|
2023
|
+
}
|
|
2770
2024
|
return storage.run({ current: initial }, fn);
|
|
2771
2025
|
}
|
|
2772
2026
|
function setCurrentRequest(req) {
|
|
2773
2027
|
const store = storage.getStore();
|
|
2774
|
-
if (store)
|
|
2028
|
+
if (store) {
|
|
2775
2029
|
store.current = req;
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
if (syncCurrent !== null) {
|
|
2033
|
+
syncCurrent = req;
|
|
2034
|
+
}
|
|
2776
2035
|
}
|
|
2777
2036
|
function getCurrentRequest() {
|
|
2778
|
-
|
|
2037
|
+
if (!contextEnabled) {
|
|
2038
|
+
contextEnabled = true;
|
|
2039
|
+
return syncCurrent ?? undefined;
|
|
2040
|
+
}
|
|
2041
|
+
return storage.getStore()?.current ?? syncCurrent ?? undefined;
|
|
2779
2042
|
}
|
|
2780
2043
|
function request() {
|
|
2781
2044
|
const req = getCurrentRequest();
|
|
@@ -3942,13 +3205,20 @@ function registerFileBasedRouting(RouterClass) {
|
|
|
3942
3205
|
}
|
|
3943
3206
|
}
|
|
3944
3207
|
this._viewsDir = viewsDir;
|
|
3208
|
+
const existingGetPaths = new Set;
|
|
3209
|
+
for (const r of this.routes) {
|
|
3210
|
+
if (r.method === "GET") {
|
|
3211
|
+
existingGetPaths.add(r.path);
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3945
3214
|
for (const route of routes) {
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
if (!existingRoute) {
|
|
3949
|
-
this.get(route.routePath, handler, "web");
|
|
3950
|
-
this._fileBasedRoutes.push(route);
|
|
3215
|
+
if (existingGetPaths.has(route.routePath)) {
|
|
3216
|
+
continue;
|
|
3951
3217
|
}
|
|
3218
|
+
const handler = createViewHandler(route.filePath, viewsDir, config2, this.config.queryPreservation);
|
|
3219
|
+
this.get(route.routePath, handler, "web");
|
|
3220
|
+
this._fileBasedRoutes.push(route);
|
|
3221
|
+
existingGetPaths.add(route.routePath);
|
|
3952
3222
|
}
|
|
3953
3223
|
this._fileRoutesInitialized = true;
|
|
3954
3224
|
},
|
|
@@ -5696,7 +4966,11 @@ function url(name, params = {}, options = {}) {
|
|
|
5696
4966
|
consumed.add(key);
|
|
5697
4967
|
}
|
|
5698
4968
|
}
|
|
5699
|
-
|
|
4969
|
+
if (resolved.includes("?}")) {
|
|
4970
|
+
resolved = resolved.replace(/\{[^}]+\?\}/g, "").replace(/\/{2,}/g, "/");
|
|
4971
|
+
if (resolved.length > 1 && resolved.endsWith("/"))
|
|
4972
|
+
resolved = resolved.slice(0, -1);
|
|
4973
|
+
}
|
|
5700
4974
|
const missing = resolved.match(/\{([^}?]+)\}/g);
|
|
5701
4975
|
if (missing && missing.length > 0)
|
|
5702
4976
|
throw new Error(`Missing required params for route '${name}': ${missing.join(", ")}`);
|
|
@@ -5715,24 +4989,229 @@ function url(name, params = {}, options = {}) {
|
|
|
5715
4989
|
return resolved;
|
|
5716
4990
|
}
|
|
5717
4991
|
|
|
5718
|
-
// src/
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
path;
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
cacheConfig;
|
|
5725
|
-
throttleConfig;
|
|
5726
|
-
routeName;
|
|
5727
|
-
modelBindings = {};
|
|
5728
|
-
constructor(method, path, handler) {
|
|
5729
|
-
this.method = method;
|
|
5730
|
-
this.path = path;
|
|
5731
|
-
this.handler = handler;
|
|
4992
|
+
// src/utils.ts
|
|
4993
|
+
import { join as join3 } from "path";
|
|
4994
|
+
function normalizePath(path) {
|
|
4995
|
+
let normalizedPath = path.replace(/\/+/g, "/");
|
|
4996
|
+
if (!normalizedPath.startsWith("/")) {
|
|
4997
|
+
normalizedPath = `/${normalizedPath}`;
|
|
5732
4998
|
}
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
4999
|
+
if (normalizedPath.length > 1 && normalizedPath.endsWith("/")) {
|
|
5000
|
+
normalizedPath = normalizedPath.slice(0, -1);
|
|
5001
|
+
}
|
|
5002
|
+
return normalizedPath;
|
|
5003
|
+
}
|
|
5004
|
+
function toActionPath(path) {
|
|
5005
|
+
return path.replace(/\//g, "_").toLowerCase();
|
|
5006
|
+
}
|
|
5007
|
+
function isActionClass(handler) {
|
|
5008
|
+
if (typeof handler !== "function" && typeof handler !== "object") {
|
|
5009
|
+
return false;
|
|
5010
|
+
}
|
|
5011
|
+
if (typeof handler === "function" && handler.prototype) {
|
|
5012
|
+
const prototype = handler.prototype;
|
|
5013
|
+
if (typeof prototype.handle === "function") {
|
|
5014
|
+
return true;
|
|
5015
|
+
}
|
|
5016
|
+
}
|
|
5017
|
+
if (typeof handler === "object" && handler !== null && "handle" in handler && typeof handler.handle === "function") {
|
|
5018
|
+
return true;
|
|
5019
|
+
}
|
|
5020
|
+
return false;
|
|
5021
|
+
}
|
|
5022
|
+
function isRouteHandler(handler) {
|
|
5023
|
+
return typeof handler === "function" && !isActionClass(handler);
|
|
5024
|
+
}
|
|
5025
|
+
function extractParamNames(pattern) {
|
|
5026
|
+
const matches = pattern.match(/\{([^}]+)\}/g);
|
|
5027
|
+
return matches ? matches.map((m) => m.slice(1, -1).replace("?", "")) : [];
|
|
5028
|
+
}
|
|
5029
|
+
function createPathRegex(pattern) {
|
|
5030
|
+
const regexPattern = pattern.replace(/\{([^}]+)\}/g, "([^/]+)");
|
|
5031
|
+
return new RegExp(`^${regexPattern}$`);
|
|
5032
|
+
}
|
|
5033
|
+
var PARAM_SEGMENT_RE = /\{([^}:]+)(?::[^}]+)?(\?)?\}/;
|
|
5034
|
+
var OPTIONAL_SEGMENT_RE = /\{[^}]+\?\}/;
|
|
5035
|
+
var compiledPatternCache = new Map;
|
|
5036
|
+
var constraintRegexCache = new Map;
|
|
5037
|
+
function compilePattern(pattern) {
|
|
5038
|
+
const cached = compiledPatternCache.get(pattern);
|
|
5039
|
+
if (cached)
|
|
5040
|
+
return cached;
|
|
5041
|
+
const isWildcard = pattern.endsWith("/*");
|
|
5042
|
+
const normalizedPattern = normalizePath(pattern);
|
|
5043
|
+
let optionalCount = 0;
|
|
5044
|
+
const segments = normalizedPattern.split("/").filter(Boolean).map((seg) => {
|
|
5045
|
+
const isOptional = OPTIONAL_SEGMENT_RE.test(seg);
|
|
5046
|
+
if (isOptional)
|
|
5047
|
+
optionalCount++;
|
|
5048
|
+
const paramMatch = seg.match(PARAM_SEGMENT_RE);
|
|
5049
|
+
return {
|
|
5050
|
+
isParam: !!paramMatch,
|
|
5051
|
+
paramName: paramMatch ? paramMatch[1].replace("?", "") : "",
|
|
5052
|
+
isOptional,
|
|
5053
|
+
literal: seg
|
|
5054
|
+
};
|
|
5055
|
+
});
|
|
5056
|
+
const compiled = {
|
|
5057
|
+
isWildcard,
|
|
5058
|
+
wildcardBase: isWildcard ? pattern.slice(0, -2) : "",
|
|
5059
|
+
segments,
|
|
5060
|
+
optionalCount
|
|
5061
|
+
};
|
|
5062
|
+
compiledPatternCache.set(pattern, compiled);
|
|
5063
|
+
return compiled;
|
|
5064
|
+
}
|
|
5065
|
+
function getConstraintRegex(constraint) {
|
|
5066
|
+
const cached = constraintRegexCache.get(constraint);
|
|
5067
|
+
if (cached !== undefined)
|
|
5068
|
+
return cached;
|
|
5069
|
+
let regex;
|
|
5070
|
+
try {
|
|
5071
|
+
regex = new RegExp(`^${constraint}$`);
|
|
5072
|
+
} catch {
|
|
5073
|
+
regex = null;
|
|
5074
|
+
}
|
|
5075
|
+
constraintRegexCache.set(constraint, regex);
|
|
5076
|
+
return regex;
|
|
5077
|
+
}
|
|
5078
|
+
function matchPath(pattern, path, params, constraints) {
|
|
5079
|
+
const compiled = compilePattern(pattern);
|
|
5080
|
+
if (compiled.isWildcard && path.startsWith(compiled.wildcardBase)) {
|
|
5081
|
+
return true;
|
|
5082
|
+
}
|
|
5083
|
+
const patternSegments = compiled.segments;
|
|
5084
|
+
const pathSegments = normalizePath(path).split("/").filter(Boolean);
|
|
5085
|
+
if (pathSegments.length < patternSegments.length - compiled.optionalCount || pathSegments.length > patternSegments.length) {
|
|
5086
|
+
return false;
|
|
5087
|
+
}
|
|
5088
|
+
for (let i = 0;i < patternSegments.length; i++) {
|
|
5089
|
+
const seg = patternSegments[i];
|
|
5090
|
+
const pathSegment = pathSegments[i];
|
|
5091
|
+
if (pathSegment === undefined) {
|
|
5092
|
+
if (seg.isOptional) {
|
|
5093
|
+
continue;
|
|
5094
|
+
}
|
|
5095
|
+
return false;
|
|
5096
|
+
}
|
|
5097
|
+
if (seg.isParam) {
|
|
5098
|
+
params[seg.paramName] = pathSegment;
|
|
5099
|
+
if (constraints && constraints[seg.paramName]) {
|
|
5100
|
+
const regex = getConstraintRegex(constraints[seg.paramName]);
|
|
5101
|
+
if (regex) {
|
|
5102
|
+
if (!regex.test(pathSegment)) {
|
|
5103
|
+
return false;
|
|
5104
|
+
}
|
|
5105
|
+
} else {
|
|
5106
|
+
console.warn(`Invalid constraint regex for parameter ${seg.paramName}: ${constraints[seg.paramName]}`);
|
|
5107
|
+
}
|
|
5108
|
+
}
|
|
5109
|
+
} else if (seg.literal !== pathSegment) {
|
|
5110
|
+
return false;
|
|
5111
|
+
}
|
|
5112
|
+
}
|
|
5113
|
+
return true;
|
|
5114
|
+
}
|
|
5115
|
+
function joinPaths(...segments) {
|
|
5116
|
+
return normalizePath(segments.join("/"));
|
|
5117
|
+
}
|
|
5118
|
+
function validatePath(path) {
|
|
5119
|
+
if (!path.startsWith("/")) {
|
|
5120
|
+
return false;
|
|
5121
|
+
}
|
|
5122
|
+
const stack = [];
|
|
5123
|
+
for (const char of path) {
|
|
5124
|
+
if (char === "{") {
|
|
5125
|
+
stack.push(char);
|
|
5126
|
+
} else if (char === "}") {
|
|
5127
|
+
if (stack.length === 0 || stack.pop() !== "{") {
|
|
5128
|
+
return false;
|
|
5129
|
+
}
|
|
5130
|
+
}
|
|
5131
|
+
}
|
|
5132
|
+
return stack.length === 0;
|
|
5133
|
+
}
|
|
5134
|
+
async function fileExists(path) {
|
|
5135
|
+
try {
|
|
5136
|
+
const file = Bun.file(path);
|
|
5137
|
+
return await file.exists();
|
|
5138
|
+
} catch {
|
|
5139
|
+
return false;
|
|
5140
|
+
}
|
|
5141
|
+
}
|
|
5142
|
+
async function resolveViewPath(viewName, viewsPath, extensions) {
|
|
5143
|
+
if (extensions.some((ext) => viewName.endsWith(ext))) {
|
|
5144
|
+
const fullPath = normalizePath(join3(viewsPath, viewName));
|
|
5145
|
+
if (await fileExists(fullPath)) {
|
|
5146
|
+
return fullPath;
|
|
5147
|
+
}
|
|
5148
|
+
return null;
|
|
5149
|
+
}
|
|
5150
|
+
for (const ext of extensions) {
|
|
5151
|
+
const fullPath = normalizePath(join3(viewsPath, `${viewName}${ext}`));
|
|
5152
|
+
if (await fileExists(fullPath)) {
|
|
5153
|
+
return fullPath;
|
|
5154
|
+
}
|
|
5155
|
+
}
|
|
5156
|
+
return null;
|
|
5157
|
+
}
|
|
5158
|
+
function processHtmlTemplate(template, data) {
|
|
5159
|
+
let result = template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
|
|
5160
|
+
const trimmedKey = key.trim();
|
|
5161
|
+
const value = getNestedValue(data, trimmedKey);
|
|
5162
|
+
return value !== undefined ? String(value) : match;
|
|
5163
|
+
});
|
|
5164
|
+
result = processConditionals(result, data);
|
|
5165
|
+
result = processLoops(result, data);
|
|
5166
|
+
return result;
|
|
5167
|
+
}
|
|
5168
|
+
function processConditionals(template, data) {
|
|
5169
|
+
return template.replace(/\{\{#if ([^}]+)\}\}([\s\S]*?)(?:\{\{else\}\}([\s\S]*?))?\{\{\/if\}\}/g, (_match, condition, content, alternative = "") => {
|
|
5170
|
+
const conditionTrimmed = condition.trim();
|
|
5171
|
+
const value = getNestedValue(data, conditionTrimmed);
|
|
5172
|
+
return value ? content : alternative;
|
|
5173
|
+
});
|
|
5174
|
+
}
|
|
5175
|
+
function processLoops(template, data) {
|
|
5176
|
+
return template.replace(/\{\{#each ([^}]+)\}\}([\s\S]*?)\{\{\/each\}\}/g, (_match, arrayKey, content) => {
|
|
5177
|
+
const array = getNestedValue(data, arrayKey.trim());
|
|
5178
|
+
if (!Array.isArray(array))
|
|
5179
|
+
return "";
|
|
5180
|
+
return array.map((item) => {
|
|
5181
|
+
return content.replace(/\{\{@index\}\}/g, String(array.indexOf(item))).replace(/\{\{this\}\}/g, String(item)).replace(/\{\{this\.([^}]+)\}\}/g, (_matchStr, prop) => {
|
|
5182
|
+
const value = item[prop.trim()];
|
|
5183
|
+
return value !== undefined ? String(value) : "";
|
|
5184
|
+
});
|
|
5185
|
+
}).join("");
|
|
5186
|
+
});
|
|
5187
|
+
}
|
|
5188
|
+
function getNestedValue(obj, path) {
|
|
5189
|
+
return path.split(".").reduce((prev, curr) => {
|
|
5190
|
+
if (prev && typeof prev === "object" && curr in prev) {
|
|
5191
|
+
return prev[curr];
|
|
5192
|
+
}
|
|
5193
|
+
return;
|
|
5194
|
+
}, obj);
|
|
5195
|
+
}
|
|
5196
|
+
|
|
5197
|
+
// src/router/fluent-routing.ts
|
|
5198
|
+
class FluentRouteBuilder {
|
|
5199
|
+
method;
|
|
5200
|
+
path;
|
|
5201
|
+
handler;
|
|
5202
|
+
middleware = [];
|
|
5203
|
+
cacheConfig;
|
|
5204
|
+
throttleConfig;
|
|
5205
|
+
routeName;
|
|
5206
|
+
modelBindings = {};
|
|
5207
|
+
constructor(method, path, handler) {
|
|
5208
|
+
this.method = method;
|
|
5209
|
+
this.path = path;
|
|
5210
|
+
this.handler = handler;
|
|
5211
|
+
}
|
|
5212
|
+
addMiddleware(middleware2) {
|
|
5213
|
+
if (Array.isArray(middleware2)) {
|
|
5214
|
+
this.middleware.push(...middleware2);
|
|
5736
5215
|
} else {
|
|
5737
5216
|
this.middleware.push(middleware2);
|
|
5738
5217
|
}
|
|
@@ -5801,14 +5280,13 @@ class FluentConditionalBuilder {
|
|
|
5801
5280
|
}
|
|
5802
5281
|
middleware(middlewareName) {
|
|
5803
5282
|
const [name, params] = middlewareName.split(":");
|
|
5804
|
-
const middlewareFactory = this.router.
|
|
5283
|
+
const middlewareFactory = this.router.resolveNamedMiddleware(name);
|
|
5805
5284
|
if (!middlewareFactory) {
|
|
5806
5285
|
throw new Error(`Unknown middleware: ${name}`);
|
|
5807
5286
|
}
|
|
5808
|
-
|
|
5809
|
-
this.router.conditionalMiddleware.push({
|
|
5287
|
+
this.router.addConditionalMiddleware({
|
|
5810
5288
|
condition: this.condition,
|
|
5811
|
-
middleware: [
|
|
5289
|
+
middleware: [middlewareFactory(params)]
|
|
5812
5290
|
});
|
|
5813
5291
|
return this;
|
|
5814
5292
|
}
|
|
@@ -5937,6 +5415,12 @@ class FluentRouter {
|
|
|
5937
5415
|
when(condition) {
|
|
5938
5416
|
return new FluentConditionalBuilder(this, condition);
|
|
5939
5417
|
}
|
|
5418
|
+
resolveNamedMiddleware(name) {
|
|
5419
|
+
return this.namedMiddleware.get(name);
|
|
5420
|
+
}
|
|
5421
|
+
addConditionalMiddleware(conditional) {
|
|
5422
|
+
this.conditionalMiddleware.push(conditional);
|
|
5423
|
+
}
|
|
5940
5424
|
middleware(middlewareName) {
|
|
5941
5425
|
const [name, params] = middlewareName.split(":");
|
|
5942
5426
|
const middlewareFactory = this.namedMiddleware.get(name);
|
|
@@ -6058,7 +5542,6 @@ class FluentRouter {
|
|
|
6058
5542
|
return [...this.routes, ...this.subdomainRouter.getAllRoutes()];
|
|
6059
5543
|
}
|
|
6060
5544
|
async handle(request2) {
|
|
6061
|
-
const _url = new URL(request2.url);
|
|
6062
5545
|
const url2 = new URL(request2.url);
|
|
6063
5546
|
const domain = url2.hostname;
|
|
6064
5547
|
const match = this.subdomainRouter.findDomainGroup(domain);
|
|
@@ -6066,34 +5549,42 @@ class FluentRouter {
|
|
|
6066
5549
|
const enhancedReq = request2;
|
|
6067
5550
|
enhancedReq.domainParams = match.parameters;
|
|
6068
5551
|
for (const route of match.group.getRoutes()) {
|
|
6069
|
-
|
|
6070
|
-
|
|
5552
|
+
const params = this.matchRouteParams(route, request2.method, url2.pathname);
|
|
5553
|
+
if (params) {
|
|
5554
|
+
return await this.executeRoute(route, enhancedReq, params);
|
|
6071
5555
|
}
|
|
6072
5556
|
}
|
|
6073
5557
|
}
|
|
6074
5558
|
for (const route of this.routes) {
|
|
6075
|
-
|
|
6076
|
-
|
|
5559
|
+
const params = this.matchRouteParams(route, request2.method, url2.pathname);
|
|
5560
|
+
if (params) {
|
|
5561
|
+
return await this.executeRoute(route, request2, params);
|
|
6077
5562
|
}
|
|
6078
5563
|
}
|
|
6079
5564
|
return null;
|
|
6080
5565
|
}
|
|
6081
|
-
|
|
6082
|
-
if (route.method !==
|
|
6083
|
-
return
|
|
5566
|
+
matchRouteParams(route, method, pathname) {
|
|
5567
|
+
if (route.method !== method) {
|
|
5568
|
+
return null;
|
|
6084
5569
|
}
|
|
6085
|
-
const
|
|
6086
|
-
|
|
6087
|
-
const regex = new RegExp(`^${routePattern}$`);
|
|
6088
|
-
return regex.test(url2.pathname);
|
|
5570
|
+
const params = {};
|
|
5571
|
+
return matchPath(route.path, pathname, params) ? params : null;
|
|
6089
5572
|
}
|
|
6090
|
-
async executeRoute(route, request2) {
|
|
6091
|
-
|
|
5573
|
+
async executeRoute(route, request2, params) {
|
|
5574
|
+
request2.params = params;
|
|
5575
|
+
const allMiddleware = [...this.globalMiddleware];
|
|
5576
|
+
for (const conditional of this.conditionalMiddleware) {
|
|
5577
|
+
if (await conditional.condition(request2)) {
|
|
5578
|
+
allMiddleware.push(...conditional.middleware);
|
|
5579
|
+
}
|
|
5580
|
+
}
|
|
5581
|
+
allMiddleware.push(...route.middleware);
|
|
6092
5582
|
let index = 0;
|
|
6093
5583
|
const next = async () => {
|
|
6094
5584
|
if (index < allMiddleware.length) {
|
|
6095
5585
|
const middleware2 = allMiddleware[index++];
|
|
6096
|
-
|
|
5586
|
+
const result = await middleware2(request2, next);
|
|
5587
|
+
return result ?? new Response(null);
|
|
6097
5588
|
}
|
|
6098
5589
|
return await route.handler(request2);
|
|
6099
5590
|
};
|
|
@@ -6133,9 +5624,16 @@ var RouteFactory = {
|
|
|
6133
5624
|
};
|
|
6134
5625
|
var RouterUtils = {
|
|
6135
5626
|
route: (name, params = {}, query = {}) => {
|
|
6136
|
-
let url2 = `/${name}`;
|
|
5627
|
+
let url2 = getNamedRoutePath(name) ?? `/${name}`;
|
|
6137
5628
|
for (const [key, value] of Object.entries(params)) {
|
|
6138
|
-
|
|
5629
|
+
const encoded = encodeURIComponent(value);
|
|
5630
|
+
url2 = url2.replace(`{${key}}`, encoded);
|
|
5631
|
+
url2 = url2.replace(`{${key}?}`, encoded);
|
|
5632
|
+
}
|
|
5633
|
+
if (url2.includes("?}")) {
|
|
5634
|
+
url2 = url2.replace(/\{[^}]+\?\}/g, "").replace(/\/{2,}/g, "/");
|
|
5635
|
+
if (url2.length > 1 && url2.endsWith("/"))
|
|
5636
|
+
url2 = url2.slice(0, -1);
|
|
6139
5637
|
}
|
|
6140
5638
|
const queryString = new URLSearchParams(query).toString();
|
|
6141
5639
|
if (queryString) {
|
|
@@ -6158,171 +5656,6 @@ var RouterUtils = {
|
|
|
6158
5656
|
};
|
|
6159
5657
|
var router = new FluentRouter;
|
|
6160
5658
|
|
|
6161
|
-
// src/utils.ts
|
|
6162
|
-
import { join as join3 } from "path";
|
|
6163
|
-
function normalizePath(path) {
|
|
6164
|
-
let normalizedPath = path.replace(/\/+/g, "/");
|
|
6165
|
-
if (!normalizedPath.startsWith("/")) {
|
|
6166
|
-
normalizedPath = `/${normalizedPath}`;
|
|
6167
|
-
}
|
|
6168
|
-
if (normalizedPath.length > 1 && normalizedPath.endsWith("/")) {
|
|
6169
|
-
normalizedPath = normalizedPath.slice(0, -1);
|
|
6170
|
-
}
|
|
6171
|
-
return normalizedPath;
|
|
6172
|
-
}
|
|
6173
|
-
function toActionPath(path) {
|
|
6174
|
-
return path.replace(/\//g, "_").toLowerCase();
|
|
6175
|
-
}
|
|
6176
|
-
function isActionClass(handler) {
|
|
6177
|
-
if (typeof handler !== "function" && typeof handler !== "object") {
|
|
6178
|
-
return false;
|
|
6179
|
-
}
|
|
6180
|
-
if (typeof handler === "function" && handler.prototype) {
|
|
6181
|
-
const prototype = handler.prototype;
|
|
6182
|
-
if (typeof prototype.handle === "function") {
|
|
6183
|
-
return true;
|
|
6184
|
-
}
|
|
6185
|
-
}
|
|
6186
|
-
if (typeof handler === "object" && handler !== null && "handle" in handler && typeof handler.handle === "function") {
|
|
6187
|
-
return true;
|
|
6188
|
-
}
|
|
6189
|
-
return false;
|
|
6190
|
-
}
|
|
6191
|
-
function isRouteHandler(handler) {
|
|
6192
|
-
return typeof handler === "function" && !isActionClass(handler);
|
|
6193
|
-
}
|
|
6194
|
-
function extractParamNames(pattern) {
|
|
6195
|
-
const matches = pattern.match(/\{([^}]+)\}/g);
|
|
6196
|
-
return matches ? matches.map((m) => m.slice(1, -1).replace("?", "")) : [];
|
|
6197
|
-
}
|
|
6198
|
-
function createPathRegex(pattern) {
|
|
6199
|
-
const regexPattern = pattern.replace(/\{([^}]+)\}/g, "([^/]+)");
|
|
6200
|
-
return new RegExp(`^${regexPattern}$`);
|
|
6201
|
-
}
|
|
6202
|
-
function matchPath(pattern, path, params, constraints) {
|
|
6203
|
-
const normalizedPattern = normalizePath(pattern);
|
|
6204
|
-
const normalizedPath = normalizePath(path);
|
|
6205
|
-
if (pattern.endsWith("/*")) {
|
|
6206
|
-
const basePattern = pattern.slice(0, -2);
|
|
6207
|
-
if (path.startsWith(basePattern)) {
|
|
6208
|
-
return true;
|
|
6209
|
-
}
|
|
6210
|
-
}
|
|
6211
|
-
const patternSegments = normalizedPattern.split("/").filter(Boolean);
|
|
6212
|
-
const pathSegments = normalizedPath.split("/").filter(Boolean);
|
|
6213
|
-
const optionalParamCount = (normalizedPattern.match(/\{[^}]+\?\}/g) || []).length;
|
|
6214
|
-
if (pathSegments.length < patternSegments.length - optionalParamCount || pathSegments.length > patternSegments.length) {
|
|
6215
|
-
return false;
|
|
6216
|
-
}
|
|
6217
|
-
for (let i = 0;i < patternSegments.length; i++) {
|
|
6218
|
-
const patternSegment = patternSegments[i];
|
|
6219
|
-
const pathSegment = pathSegments[i];
|
|
6220
|
-
if (pathSegment === undefined) {
|
|
6221
|
-
if (patternSegment.match(/\{[^}]+\?\}/)) {
|
|
6222
|
-
continue;
|
|
6223
|
-
} else {
|
|
6224
|
-
return false;
|
|
6225
|
-
}
|
|
6226
|
-
}
|
|
6227
|
-
const paramMatch = patternSegment.match(/\{([^}:]+)(?::[^}]+)?(\?)?\}/);
|
|
6228
|
-
if (paramMatch) {
|
|
6229
|
-
const paramName = paramMatch[1].replace("?", "");
|
|
6230
|
-
params[paramName] = pathSegment;
|
|
6231
|
-
if (constraints && constraints[paramName]) {
|
|
6232
|
-
try {
|
|
6233
|
-
const regex = new RegExp(`^${constraints[paramName]}$`);
|
|
6234
|
-
if (!regex.test(pathSegment)) {
|
|
6235
|
-
return false;
|
|
6236
|
-
}
|
|
6237
|
-
} catch {
|
|
6238
|
-
console.warn(`Invalid constraint regex for parameter ${paramName}: ${constraints[paramName]}`);
|
|
6239
|
-
}
|
|
6240
|
-
}
|
|
6241
|
-
} else if (patternSegment !== pathSegment) {
|
|
6242
|
-
return false;
|
|
6243
|
-
}
|
|
6244
|
-
}
|
|
6245
|
-
return true;
|
|
6246
|
-
}
|
|
6247
|
-
function joinPaths(...segments) {
|
|
6248
|
-
return normalizePath(segments.join("/"));
|
|
6249
|
-
}
|
|
6250
|
-
function validatePath(path) {
|
|
6251
|
-
if (!path.startsWith("/")) {
|
|
6252
|
-
return false;
|
|
6253
|
-
}
|
|
6254
|
-
const stack = [];
|
|
6255
|
-
for (const char of path) {
|
|
6256
|
-
if (char === "{") {
|
|
6257
|
-
stack.push(char);
|
|
6258
|
-
} else if (char === "}") {
|
|
6259
|
-
if (stack.length === 0 || stack.pop() !== "{") {
|
|
6260
|
-
return false;
|
|
6261
|
-
}
|
|
6262
|
-
}
|
|
6263
|
-
}
|
|
6264
|
-
return stack.length === 0;
|
|
6265
|
-
}
|
|
6266
|
-
async function fileExists(path) {
|
|
6267
|
-
try {
|
|
6268
|
-
const file = Bun.file(path);
|
|
6269
|
-
return await file.exists();
|
|
6270
|
-
} catch {
|
|
6271
|
-
return false;
|
|
6272
|
-
}
|
|
6273
|
-
}
|
|
6274
|
-
async function resolveViewPath(viewName, viewsPath, extensions) {
|
|
6275
|
-
if (extensions.some((ext) => viewName.endsWith(ext))) {
|
|
6276
|
-
const fullPath = normalizePath(join3(viewsPath, viewName));
|
|
6277
|
-
if (await fileExists(fullPath)) {
|
|
6278
|
-
return fullPath;
|
|
6279
|
-
}
|
|
6280
|
-
return null;
|
|
6281
|
-
}
|
|
6282
|
-
for (const ext of extensions) {
|
|
6283
|
-
const fullPath = normalizePath(join3(viewsPath, `${viewName}${ext}`));
|
|
6284
|
-
if (await fileExists(fullPath)) {
|
|
6285
|
-
return fullPath;
|
|
6286
|
-
}
|
|
6287
|
-
}
|
|
6288
|
-
return null;
|
|
6289
|
-
}
|
|
6290
|
-
function processHtmlTemplate(template, data) {
|
|
6291
|
-
let result = template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
|
|
6292
|
-
const trimmedKey = key.trim();
|
|
6293
|
-
const value = getNestedValue(data, trimmedKey);
|
|
6294
|
-
return value !== undefined ? String(value) : match;
|
|
6295
|
-
});
|
|
6296
|
-
result = processConditionals(result, data);
|
|
6297
|
-
result = processLoops(result, data);
|
|
6298
|
-
return result;
|
|
6299
|
-
}
|
|
6300
|
-
function processConditionals(template, data) {
|
|
6301
|
-
return template.replace(/\{\{#if ([^}]+)\}\}([\s\S]*?)(?:\{\{else\}\}([\s\S]*?))?\{\{\/if\}\}/g, (_match, condition, content, alternative = "") => {
|
|
6302
|
-
const conditionTrimmed = condition.trim();
|
|
6303
|
-
const value = getNestedValue(data, conditionTrimmed);
|
|
6304
|
-
return value ? content : alternative;
|
|
6305
|
-
});
|
|
6306
|
-
}
|
|
6307
|
-
function processLoops(template, data) {
|
|
6308
|
-
return template.replace(/\{\{#each ([^}]+)\}\}([\s\S]*?)\{\{\/each\}\}/g, (_match, arrayKey, content) => {
|
|
6309
|
-
const array = getNestedValue(data, arrayKey.trim());
|
|
6310
|
-
if (!Array.isArray(array))
|
|
6311
|
-
return "";
|
|
6312
|
-
return array.map((item) => {
|
|
6313
|
-
return content.replace(/\{\{@index\}\}/g, String(array.indexOf(item))).replace(/\{\{this\}\}/g, String(item)).replace(/\{\{this\.([^}]+)\}\}/g, (_matchStr, prop) => {
|
|
6314
|
-
const value = item[prop.trim()];
|
|
6315
|
-
return value !== undefined ? String(value) : "";
|
|
6316
|
-
});
|
|
6317
|
-
}).join("");
|
|
6318
|
-
});
|
|
6319
|
-
}
|
|
6320
|
-
function getNestedValue(obj, path) {
|
|
6321
|
-
return path.split(".").reduce((prev, curr) => {
|
|
6322
|
-
return prev && prev[curr] !== undefined ? prev[curr] : undefined;
|
|
6323
|
-
}, obj);
|
|
6324
|
-
}
|
|
6325
|
-
|
|
6326
5659
|
// src/router/group-organization.ts
|
|
6327
5660
|
function registerGroupOrganization(RouterClass) {
|
|
6328
5661
|
Object.defineProperties(RouterClass.prototype, {
|
|
@@ -6342,7 +5675,16 @@ function registerGroupOrganization(RouterClass) {
|
|
|
6342
5675
|
];
|
|
6343
5676
|
}
|
|
6344
5677
|
this.currentGroup = newGroup;
|
|
6345
|
-
callback();
|
|
5678
|
+
const result = callback();
|
|
5679
|
+
if (result instanceof Promise) {
|
|
5680
|
+
return result.then(() => {
|
|
5681
|
+
this.currentGroup = previousGroup;
|
|
5682
|
+
return this;
|
|
5683
|
+
}, (error) => {
|
|
5684
|
+
this.currentGroup = previousGroup;
|
|
5685
|
+
throw error;
|
|
5686
|
+
});
|
|
5687
|
+
}
|
|
6346
5688
|
this.currentGroup = previousGroup;
|
|
6347
5689
|
return this;
|
|
6348
5690
|
},
|
|
@@ -6539,10 +5881,11 @@ function registerHttpMethods(RouterClass) {
|
|
|
6539
5881
|
this.namedRoutes.set(name, route);
|
|
6540
5882
|
registerNamedRoute(name, route.path);
|
|
6541
5883
|
}
|
|
6542
|
-
if (this.addRouteToCompiler) {
|
|
5884
|
+
if (this.addRouteToCompiler && !domain) {
|
|
6543
5885
|
this.addRouteToCompiler(route);
|
|
6544
5886
|
}
|
|
6545
5887
|
this.routeCache.clear();
|
|
5888
|
+
this._allowedMethodsCache?.clear();
|
|
6546
5889
|
return this;
|
|
6547
5890
|
},
|
|
6548
5891
|
writable: true,
|
|
@@ -6590,6 +5933,13 @@ function registerHttpMethods(RouterClass) {
|
|
|
6590
5933
|
writable: true,
|
|
6591
5934
|
configurable: true
|
|
6592
5935
|
},
|
|
5936
|
+
head: {
|
|
5937
|
+
value(path, handler, type, name, middleware2) {
|
|
5938
|
+
return this.addRoute("HEAD", path, handler, type, name, middleware2);
|
|
5939
|
+
},
|
|
5940
|
+
writable: true,
|
|
5941
|
+
configurable: true
|
|
5942
|
+
},
|
|
6593
5943
|
match: {
|
|
6594
5944
|
value(methods, path, handler, type, name, middleware2) {
|
|
6595
5945
|
for (const method of methods) {
|
|
@@ -6627,6 +5977,12 @@ function registerHttpMethods(RouterClass) {
|
|
|
6627
5977
|
url2 = url2.replace(`{${param}}`, encodeURIComponent(value));
|
|
6628
5978
|
url2 = url2.replace(`{${param}?}`, encodeURIComponent(value));
|
|
6629
5979
|
}
|
|
5980
|
+
if (url2.includes("{")) {
|
|
5981
|
+
url2 = url2.replace(/\{[^}]+\?\}/g, "").replace(/\/{2,}/g, "/");
|
|
5982
|
+
if (url2.length > 1 && url2.endsWith("/")) {
|
|
5983
|
+
url2 = url2.slice(0, -1);
|
|
5984
|
+
}
|
|
5985
|
+
}
|
|
6630
5986
|
return url2;
|
|
6631
5987
|
},
|
|
6632
5988
|
writable: true,
|
|
@@ -6837,6 +6193,19 @@ Last error: ${lastError?.message || "Unknown error"}`);
|
|
|
6837
6193
|
function createHandlerResolver(config2) {
|
|
6838
6194
|
return (handler, req) => resolveHandler(handler, req, config2);
|
|
6839
6195
|
}
|
|
6196
|
+
function createHandlerInvoker(handler, config2) {
|
|
6197
|
+
if (handler instanceof Response) {
|
|
6198
|
+
return async () => handler.clone();
|
|
6199
|
+
}
|
|
6200
|
+
if (isCallableHandler(handler)) {
|
|
6201
|
+
return async (req) => wrapResponse(await handler(req));
|
|
6202
|
+
}
|
|
6203
|
+
if (typeof handler === "function" && handler.prototype && typeof handler.prototype.handle === "function") {
|
|
6204
|
+
const HandlerClass = handler;
|
|
6205
|
+
return async (req) => wrapResponse(await new HandlerClass().handle(req));
|
|
6206
|
+
}
|
|
6207
|
+
return (req) => resolveHandler(handler, req, config2);
|
|
6208
|
+
}
|
|
6840
6209
|
|
|
6841
6210
|
// src/router/middleware.ts
|
|
6842
6211
|
function registerMiddlewareHandling(RouterClass) {
|
|
@@ -6849,6 +6218,7 @@ function registerMiddlewareHandling(RouterClass) {
|
|
|
6849
6218
|
this.globalMiddleware.push(resolvedMiddleware);
|
|
6850
6219
|
}
|
|
6851
6220
|
}
|
|
6221
|
+
this._mwEpoch = (this._mwEpoch || 0) + 1;
|
|
6852
6222
|
return this;
|
|
6853
6223
|
},
|
|
6854
6224
|
writable: true,
|
|
@@ -7495,40 +6865,36 @@ class RouteTrie {
|
|
|
7495
6865
|
return null;
|
|
7496
6866
|
}
|
|
7497
6867
|
const segment = segments[segmentIndex];
|
|
7498
|
-
const
|
|
7499
|
-
if (
|
|
7500
|
-
|
|
7501
|
-
|
|
7502
|
-
|
|
7503
|
-
});
|
|
7504
|
-
}
|
|
7505
|
-
if (node.paramChild) {
|
|
7506
|
-
const paramNode = node.paramChild;
|
|
7507
|
-
let matches = true;
|
|
7508
|
-
if (paramNode.pattern) {
|
|
7509
|
-
matches = paramNode.pattern.test(segment);
|
|
7510
|
-
}
|
|
7511
|
-
if (matches && paramNode.paramName) {
|
|
7512
|
-
candidates.push({
|
|
7513
|
-
node: paramNode,
|
|
7514
|
-
newParams: { ...params, [paramNode.paramName]: segment }
|
|
7515
|
-
});
|
|
7516
|
-
}
|
|
6868
|
+
const staticChild = node.children.get(segment);
|
|
6869
|
+
if (staticChild) {
|
|
6870
|
+
const match = this.matchSegments(staticChild, segments, segmentIndex + 1, params, method);
|
|
6871
|
+
if (match)
|
|
6872
|
+
return match;
|
|
7517
6873
|
}
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
const
|
|
6874
|
+
const paramNode = node.paramChild;
|
|
6875
|
+
if (paramNode && paramNode.paramName && (!paramNode.pattern || paramNode.pattern.test(segment))) {
|
|
6876
|
+
const previous = params[paramNode.paramName];
|
|
6877
|
+
params[paramNode.paramName] = segment;
|
|
6878
|
+
const match = this.matchSegments(paramNode, segments, segmentIndex + 1, params, method);
|
|
7521
6879
|
if (match)
|
|
7522
6880
|
return match;
|
|
6881
|
+
if (previous === undefined)
|
|
6882
|
+
delete params[paramNode.paramName];
|
|
6883
|
+
else
|
|
6884
|
+
params[paramNode.paramName] = previous;
|
|
7523
6885
|
}
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
|
|
6886
|
+
if (node.wildcardChild) {
|
|
6887
|
+
const compiled = node.wildcardChild.getRoute(method);
|
|
6888
|
+
if (compiled) {
|
|
6889
|
+
params.wildcard = segments.slice(segmentIndex).join("/");
|
|
6890
|
+
return {
|
|
6891
|
+
route: compiled.route,
|
|
6892
|
+
params,
|
|
6893
|
+
score: compiled.priority
|
|
6894
|
+
};
|
|
7529
6895
|
}
|
|
7530
6896
|
}
|
|
7531
|
-
return
|
|
6897
|
+
return null;
|
|
7532
6898
|
}
|
|
7533
6899
|
getAllRoutes() {
|
|
7534
6900
|
return Array.from(this.compiledRoutes.values());
|
|
@@ -7582,7 +6948,7 @@ class RouteTrie {
|
|
|
7582
6948
|
class RouteCompiler {
|
|
7583
6949
|
trie;
|
|
7584
6950
|
matchCache = new Map;
|
|
7585
|
-
|
|
6951
|
+
routeKeys = new Set;
|
|
7586
6952
|
stats = {
|
|
7587
6953
|
totalMatches: 0,
|
|
7588
6954
|
cacheHits: 0,
|
|
@@ -7599,6 +6965,7 @@ class RouteCompiler {
|
|
|
7599
6965
|
enablePriorityOptimization: true,
|
|
7600
6966
|
cacheSize: 1000,
|
|
7601
6967
|
precompilePatterns: true,
|
|
6968
|
+
enableProfiling: false,
|
|
7602
6969
|
...options
|
|
7603
6970
|
};
|
|
7604
6971
|
this.trie = new RouteTrie;
|
|
@@ -7607,10 +6974,11 @@ class RouteCompiler {
|
|
|
7607
6974
|
if (this.options.precompilePatterns) {
|
|
7608
6975
|
this.precompileRoutePattern(route);
|
|
7609
6976
|
}
|
|
7610
|
-
const
|
|
7611
|
-
if (
|
|
6977
|
+
const routeKey = `${route.method}:${route.path}`;
|
|
6978
|
+
if (this.routeKeys.has(routeKey)) {
|
|
7612
6979
|
return false;
|
|
7613
6980
|
}
|
|
6981
|
+
this.routeKeys.add(routeKey);
|
|
7614
6982
|
this.trie.addRoute(route);
|
|
7615
6983
|
if (this.options.enableCaching) {
|
|
7616
6984
|
this.matchCache.clear();
|
|
@@ -7619,15 +6987,6 @@ class RouteCompiler {
|
|
|
7619
6987
|
this.stats.methodDistribution[method] = (this.stats.methodDistribution[method] || 0) + 1;
|
|
7620
6988
|
return true;
|
|
7621
6989
|
}
|
|
7622
|
-
checkForDuplicateRoute(route) {
|
|
7623
|
-
const routes = this.trie.getAllRoutes();
|
|
7624
|
-
for (const existingRoute of routes) {
|
|
7625
|
-
if (existingRoute.route.path === route.path && existingRoute.route.method === route.method) {
|
|
7626
|
-
return true;
|
|
7627
|
-
}
|
|
7628
|
-
}
|
|
7629
|
-
return false;
|
|
7630
|
-
}
|
|
7631
6990
|
patternCache = new Map;
|
|
7632
6991
|
extractParamNames(path) {
|
|
7633
6992
|
const paramNames = [];
|
|
@@ -7670,17 +7029,25 @@ class RouteCompiler {
|
|
|
7670
7029
|
}
|
|
7671
7030
|
}
|
|
7672
7031
|
convertToURLPattern(path, constraints) {
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7032
|
+
const paramTokens = [];
|
|
7033
|
+
const withTokens = path.replace(/\{([^}]+)\}/g, (_m, inner) => {
|
|
7034
|
+
paramTokens.push(inner);
|
|
7035
|
+
return `\x00${paramTokens.length - 1}\x00`;
|
|
7036
|
+
});
|
|
7037
|
+
let regexPattern = withTokens.replace(/[.+*?^${}()|[\]\\]/g, "\\$&");
|
|
7038
|
+
regexPattern = regexPattern.replace(/\/?\u0000(\d+)\u0000/g, (token, idx) => {
|
|
7039
|
+
const inner = paramTokens[Number(idx)];
|
|
7040
|
+
const isOptional = inner.endsWith("?");
|
|
7041
|
+
const core = isOptional ? inner.slice(0, -1) : inner;
|
|
7042
|
+
const colonIndex = core.indexOf(":");
|
|
7043
|
+
const name = colonIndex === -1 ? core : core.slice(0, colonIndex);
|
|
7044
|
+
const inlinePattern = colonIndex === -1 ? undefined : core.slice(colonIndex + 1);
|
|
7045
|
+
const pattern = constraints?.[name] ?? inlinePattern ?? "[^/]+";
|
|
7046
|
+
const group = `(?<${name}>${pattern})`;
|
|
7047
|
+
const leadingSlash = token.startsWith("/") ? "/" : "";
|
|
7048
|
+
return isOptional ? `(?:${leadingSlash}${group})?` : `${leadingSlash}${group}`;
|
|
7681
7049
|
});
|
|
7682
|
-
|
|
7683
|
-
const regex = new RegExp(regexPattern);
|
|
7050
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
7684
7051
|
return {
|
|
7685
7052
|
exec: (pathname) => {
|
|
7686
7053
|
const match = pathname.match(regex);
|
|
@@ -7693,16 +7060,16 @@ class RouteCompiler {
|
|
|
7693
7060
|
};
|
|
7694
7061
|
}
|
|
7695
7062
|
match(path, method) {
|
|
7696
|
-
const startTime = performance.now();
|
|
7063
|
+
const startTime = this.options.enableProfiling ? performance.now() : 0;
|
|
7697
7064
|
this.stats.totalMatches++;
|
|
7698
7065
|
if (this.options.enableCaching) {
|
|
7699
7066
|
const cacheKey = `${method}:${path}`;
|
|
7700
|
-
|
|
7067
|
+
const cached = this.matchCache.get(cacheKey);
|
|
7068
|
+
if (cached !== undefined || this.matchCache.has(cacheKey)) {
|
|
7701
7069
|
this.stats.cacheHits++;
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
return cached || null;
|
|
7070
|
+
if (startTime)
|
|
7071
|
+
this.updateMatchTime(startTime);
|
|
7072
|
+
return cached ? { route: cached.route, params: { ...cached.params } } : null;
|
|
7706
7073
|
}
|
|
7707
7074
|
this.stats.cacheMisses++;
|
|
7708
7075
|
}
|
|
@@ -7718,25 +7085,18 @@ class RouteCompiler {
|
|
|
7718
7085
|
const cacheKey = `${method}:${path}`;
|
|
7719
7086
|
this.addToCache(cacheKey, result);
|
|
7720
7087
|
}
|
|
7721
|
-
|
|
7722
|
-
|
|
7088
|
+
if (startTime)
|
|
7089
|
+
this.updateMatchTime(startTime);
|
|
7090
|
+
return result ? { route: result.route, params: { ...result.params } } : null;
|
|
7723
7091
|
}
|
|
7724
7092
|
addToCache(key, value) {
|
|
7725
7093
|
if (this.matchCache.size >= this.options.cacheSize && !this.matchCache.has(key)) {
|
|
7726
|
-
|
|
7727
|
-
|
|
7094
|
+
const lruKey = this.matchCache.keys().next().value;
|
|
7095
|
+
if (lruKey !== undefined) {
|
|
7728
7096
|
this.matchCache.delete(lruKey);
|
|
7729
7097
|
}
|
|
7730
7098
|
}
|
|
7731
7099
|
this.matchCache.set(key, value);
|
|
7732
|
-
this.updateCacheLRU(key);
|
|
7733
|
-
}
|
|
7734
|
-
updateCacheLRU(key) {
|
|
7735
|
-
const index = this.cacheKeys.indexOf(key);
|
|
7736
|
-
if (index !== -1) {
|
|
7737
|
-
this.cacheKeys.splice(index, 1);
|
|
7738
|
-
}
|
|
7739
|
-
this.cacheKeys.push(key);
|
|
7740
7100
|
}
|
|
7741
7101
|
updateMatchTime(startTime) {
|
|
7742
7102
|
const matchTime = performance.now() - startTime;
|
|
@@ -7805,7 +7165,7 @@ class RouteCompiler {
|
|
|
7805
7165
|
clear() {
|
|
7806
7166
|
this.trie.clear();
|
|
7807
7167
|
this.matchCache.clear();
|
|
7808
|
-
this.
|
|
7168
|
+
this.routeKeys.clear();
|
|
7809
7169
|
this.stats = {
|
|
7810
7170
|
totalMatches: 0,
|
|
7811
7171
|
cacheHits: 0,
|
|
@@ -7999,14 +7359,25 @@ function registerOptimizedRouteMatching(RouterClass) {
|
|
|
7999
7359
|
matchRoute: {
|
|
8000
7360
|
value(path, method, domain) {
|
|
8001
7361
|
this.initializeRouteCompiler();
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
if (
|
|
8005
|
-
return;
|
|
7362
|
+
if (domain && this.domains[domain]) {
|
|
7363
|
+
const domainMatch = this.fallbackMatchRoute(path, method, domain);
|
|
7364
|
+
if (domainMatch) {
|
|
7365
|
+
return domainMatch;
|
|
7366
|
+
}
|
|
7367
|
+
} else {
|
|
7368
|
+
const match = this.routeCompiler.match(path, method);
|
|
7369
|
+
if (match && (!domain || !match.route.domain || match.route.domain === domain)) {
|
|
7370
|
+
return match;
|
|
7371
|
+
}
|
|
7372
|
+
const fallback = this.fallbackMatchRoute(path, method, domain);
|
|
7373
|
+
if (fallback) {
|
|
7374
|
+
return fallback;
|
|
8006
7375
|
}
|
|
8007
|
-
return match;
|
|
8008
7376
|
}
|
|
8009
|
-
|
|
7377
|
+
if (method === "HEAD") {
|
|
7378
|
+
return this.matchRoute(path, "GET", domain);
|
|
7379
|
+
}
|
|
7380
|
+
return;
|
|
8010
7381
|
},
|
|
8011
7382
|
writable: true,
|
|
8012
7383
|
configurable: true
|
|
@@ -8018,6 +7389,9 @@ function registerOptimizedRouteMatching(RouterClass) {
|
|
|
8018
7389
|
if (this.routeCache.has(cacheKey)) {
|
|
8019
7390
|
return this.routeCache.get(cacheKey);
|
|
8020
7391
|
}
|
|
7392
|
+
if (this.routeCache.size >= 1e4) {
|
|
7393
|
+
this.routeCache.clear();
|
|
7394
|
+
}
|
|
8021
7395
|
if (this.staticRoutes.has(method)) {
|
|
8022
7396
|
const staticRoute = this.staticRoutes.get(method).get(url2.pathname);
|
|
8023
7397
|
if (staticRoute && (!domain || !staticRoute.domain || staticRoute.domain === domain)) {
|
|
@@ -8054,6 +7428,24 @@ function registerOptimizedRouteMatching(RouterClass) {
|
|
|
8054
7428
|
}
|
|
8055
7429
|
}
|
|
8056
7430
|
}
|
|
7431
|
+
const wildcardPools = domain && this.domains[domain] ? [this.domains[domain], this.routes] : [this.routes];
|
|
7432
|
+
for (const pool of wildcardPools) {
|
|
7433
|
+
for (const route of pool) {
|
|
7434
|
+
if (route.method !== method || !route.path.endsWith("*")) {
|
|
7435
|
+
continue;
|
|
7436
|
+
}
|
|
7437
|
+
const basePath = route.path.slice(0, -1);
|
|
7438
|
+
if (url2.pathname.startsWith(basePath)) {
|
|
7439
|
+
const result = {
|
|
7440
|
+
route,
|
|
7441
|
+
params: { wildcard: url2.pathname.slice(basePath.length) }
|
|
7442
|
+
};
|
|
7443
|
+
this.routeCache.set(cacheKey, result);
|
|
7444
|
+
return result;
|
|
7445
|
+
}
|
|
7446
|
+
}
|
|
7447
|
+
}
|
|
7448
|
+
this.routeCache.set(cacheKey, undefined);
|
|
8057
7449
|
return;
|
|
8058
7450
|
},
|
|
8059
7451
|
writable: true,
|
|
@@ -8472,6 +7864,17 @@ function registerRouteMatching(RouterClass) {
|
|
|
8472
7864
|
writable: true,
|
|
8473
7865
|
configurable: true
|
|
8474
7866
|
},
|
|
7867
|
+
withoutNativeDispatch: {
|
|
7868
|
+
value() {
|
|
7869
|
+
const lastRoute = this.routes[this.routes.length - 1];
|
|
7870
|
+
if (lastRoute) {
|
|
7871
|
+
lastRoute.nativeDispatch = false;
|
|
7872
|
+
}
|
|
7873
|
+
return this;
|
|
7874
|
+
},
|
|
7875
|
+
writable: true,
|
|
7876
|
+
configurable: true
|
|
7877
|
+
},
|
|
8475
7878
|
whereNumber: {
|
|
8476
7879
|
value(param) {
|
|
8477
7880
|
return this.where({ [param]: "\\d+" });
|
|
@@ -8534,6 +7937,8 @@ class Router {
|
|
|
8534
7937
|
namedMiddleware = new Map;
|
|
8535
7938
|
conditionalMiddleware = [];
|
|
8536
7939
|
_middlewarePipeline;
|
|
7940
|
+
_mwEpoch = 0;
|
|
7941
|
+
_allowedMethodsCache = new Map;
|
|
8537
7942
|
config = {
|
|
8538
7943
|
verbose: false,
|
|
8539
7944
|
routesPath: "routes",
|
|
@@ -8634,6 +8039,7 @@ class Router {
|
|
|
8634
8039
|
}
|
|
8635
8040
|
invalidateCache() {
|
|
8636
8041
|
this.routeCache.clear();
|
|
8042
|
+
this._allowedMethodsCache.clear();
|
|
8637
8043
|
}
|
|
8638
8044
|
registerRoute(method, path, handler, type, name, middleware2) {
|
|
8639
8045
|
let routePath = path;
|
|
@@ -8948,50 +8354,44 @@ class Router {
|
|
|
8948
8354
|
}
|
|
8949
8355
|
getAllowedMethods(path, domain) {
|
|
8950
8356
|
const url2 = new URL(path, "http://localhost");
|
|
8357
|
+
const cacheKey = `${domain || ""}:${url2.pathname}`;
|
|
8358
|
+
const cached = this._allowedMethodsCache.get(cacheKey);
|
|
8359
|
+
if (cached) {
|
|
8360
|
+
return cached;
|
|
8361
|
+
}
|
|
8951
8362
|
const methods = new Set;
|
|
8952
|
-
const
|
|
8953
|
-
|
|
8954
|
-
if (
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8363
|
+
for (const [method, routesByPath] of this.staticRoutes) {
|
|
8364
|
+
const staticRoute = routesByPath.get(url2.pathname);
|
|
8365
|
+
if (staticRoute && (!domain || !staticRoute.domain || staticRoute.domain === domain)) {
|
|
8366
|
+
methods.add(method);
|
|
8367
|
+
}
|
|
8368
|
+
}
|
|
8369
|
+
const potentialRoutes = domain && this.domains[domain] ? this.domains[domain] : this.routes;
|
|
8370
|
+
for (const route of potentialRoutes) {
|
|
8371
|
+
if (methods.has(route.method)) {
|
|
8372
|
+
continue;
|
|
8960
8373
|
}
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
if (route.path === url2.pathname) {
|
|
8965
|
-
methods.add(method);
|
|
8966
|
-
break;
|
|
8967
|
-
}
|
|
8374
|
+
if (route.path === url2.pathname) {
|
|
8375
|
+
methods.add(route.method);
|
|
8376
|
+
continue;
|
|
8968
8377
|
}
|
|
8969
|
-
if (
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
const match = route.pattern.exec(url2);
|
|
8973
|
-
if (match) {
|
|
8974
|
-
methods.add(method);
|
|
8975
|
-
break;
|
|
8976
|
-
}
|
|
8977
|
-
}
|
|
8978
|
-
}
|
|
8378
|
+
if (route.pattern && route.pattern.exec(url2)) {
|
|
8379
|
+
methods.add(route.method);
|
|
8380
|
+
continue;
|
|
8979
8381
|
}
|
|
8980
|
-
if (
|
|
8981
|
-
|
|
8982
|
-
for (const route of wildcardRoutes) {
|
|
8983
|
-
const basePath = route.path.slice(0, -1);
|
|
8984
|
-
if (url2.pathname.startsWith(basePath)) {
|
|
8985
|
-
methods.add(method);
|
|
8986
|
-
break;
|
|
8987
|
-
}
|
|
8988
|
-
}
|
|
8382
|
+
if (route.path.endsWith("*") && url2.pathname.startsWith(route.path.slice(0, -1))) {
|
|
8383
|
+
methods.add(route.method);
|
|
8989
8384
|
}
|
|
8990
8385
|
}
|
|
8991
8386
|
if (methods.has("GET")) {
|
|
8992
8387
|
methods.add("HEAD");
|
|
8993
8388
|
}
|
|
8994
|
-
|
|
8389
|
+
const result = Array.from(methods);
|
|
8390
|
+
if (this._allowedMethodsCache.size >= 1e4) {
|
|
8391
|
+
this._allowedMethodsCache.clear();
|
|
8392
|
+
}
|
|
8393
|
+
this._allowedMethodsCache.set(cacheKey, result);
|
|
8394
|
+
return result;
|
|
8995
8395
|
}
|
|
8996
8396
|
matchRoute(path, method, domain) {
|
|
8997
8397
|
const url2 = new URL(path, "http://localhost");
|
|
@@ -9419,12 +8819,15 @@ class Router {
|
|
|
9419
8819
|
}
|
|
9420
8820
|
const result = callback();
|
|
9421
8821
|
if (result instanceof Promise) {
|
|
9422
|
-
result.then(() => {
|
|
8822
|
+
return result.then(() => {
|
|
8823
|
+
this.currentGroup = previousGroup;
|
|
8824
|
+
return this;
|
|
8825
|
+
}, (error) => {
|
|
9423
8826
|
this.currentGroup = previousGroup;
|
|
8827
|
+
throw error;
|
|
9424
8828
|
});
|
|
9425
|
-
} else {
|
|
9426
|
-
this.currentGroup = previousGroup;
|
|
9427
8829
|
}
|
|
8830
|
+
this.currentGroup = previousGroup;
|
|
9428
8831
|
return this;
|
|
9429
8832
|
}
|
|
9430
8833
|
addRoute(route) {
|
|
@@ -9556,10 +8959,48 @@ function applyRequestEnhancements(req, params = {}) {
|
|
|
9556
8959
|
}
|
|
9557
8960
|
|
|
9558
8961
|
// src/request/macros.ts
|
|
8962
|
+
var MOBILE_REGEX = /Mobile|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
|
|
8963
|
+
var BOT_REGEX = /bot|crawler|spider|crawling/i;
|
|
8964
|
+
var GLOB_PATTERN_CACHE = new Map;
|
|
8965
|
+
function getParsedURL(req) {
|
|
8966
|
+
const cache = req;
|
|
8967
|
+
if (!cache._parsedURL) {
|
|
8968
|
+
cache._parsedURL = new URL(req.url);
|
|
8969
|
+
}
|
|
8970
|
+
return cache._parsedURL;
|
|
8971
|
+
}
|
|
8972
|
+
function getParsedCookies(req) {
|
|
8973
|
+
const cache = req;
|
|
8974
|
+
if (!cache._parsedCookies) {
|
|
8975
|
+
const cookies = {};
|
|
8976
|
+
const cookieHeader = req.headers.get("cookie");
|
|
8977
|
+
if (cookieHeader) {
|
|
8978
|
+
for (const cookie of cookieHeader.split(";")) {
|
|
8979
|
+
const eqIndex = cookie.indexOf("=");
|
|
8980
|
+
if (eqIndex === -1)
|
|
8981
|
+
continue;
|
|
8982
|
+
const key = cookie.slice(0, eqIndex).trim();
|
|
8983
|
+
if (!key)
|
|
8984
|
+
continue;
|
|
8985
|
+
const value = cookie.slice(eqIndex + 1).trim();
|
|
8986
|
+
try {
|
|
8987
|
+
cookies[key] = decodeURIComponent(value);
|
|
8988
|
+
} catch {
|
|
8989
|
+
cookies[key] = value;
|
|
8990
|
+
}
|
|
8991
|
+
}
|
|
8992
|
+
}
|
|
8993
|
+
cache._parsedCookies = cookies;
|
|
8994
|
+
}
|
|
8995
|
+
return cache._parsedCookies;
|
|
8996
|
+
}
|
|
8997
|
+
|
|
9559
8998
|
class RequestMacroRegistry {
|
|
9560
8999
|
macros = new Map;
|
|
9561
|
-
|
|
9562
|
-
|
|
9000
|
+
epoch = 0;
|
|
9001
|
+
register(name, handler, accessor = false) {
|
|
9002
|
+
this.macros.set(name, { name, handler, accessor });
|
|
9003
|
+
this.epoch++;
|
|
9563
9004
|
}
|
|
9564
9005
|
get(name) {
|
|
9565
9006
|
return this.macros.get(name);
|
|
@@ -9571,23 +9012,71 @@ class RequestMacroRegistry {
|
|
|
9571
9012
|
return Array.from(this.macros.values());
|
|
9572
9013
|
}
|
|
9573
9014
|
remove(name) {
|
|
9574
|
-
|
|
9015
|
+
const removed = this.macros.delete(name);
|
|
9016
|
+
if (removed)
|
|
9017
|
+
this.epoch++;
|
|
9018
|
+
return removed;
|
|
9575
9019
|
}
|
|
9576
9020
|
clear() {
|
|
9577
9021
|
this.macros.clear();
|
|
9022
|
+
this.epoch++;
|
|
9023
|
+
}
|
|
9024
|
+
getEpoch() {
|
|
9025
|
+
return this.epoch;
|
|
9578
9026
|
}
|
|
9579
9027
|
}
|
|
9580
9028
|
var requestMacroRegistry = new RequestMacroRegistry;
|
|
9029
|
+
var MACRO_PROTO = Symbol("bunRouterMacroProto");
|
|
9030
|
+
var MACRO_BASE = Symbol("bunRouterMacroBase");
|
|
9031
|
+
var macroProtoCache = new WeakMap;
|
|
9032
|
+
var macroProtoEpoch = -1;
|
|
9581
9033
|
|
|
9582
9034
|
class RequestWithMacros {
|
|
9583
9035
|
static macro(name, handler) {
|
|
9584
9036
|
requestMacroRegistry.register(name, handler);
|
|
9585
9037
|
}
|
|
9038
|
+
static macroAccessor(name, getter) {
|
|
9039
|
+
requestMacroRegistry.register(name, getter, true);
|
|
9040
|
+
}
|
|
9586
9041
|
static applyMacros(request2) {
|
|
9587
|
-
const
|
|
9588
|
-
|
|
9589
|
-
|
|
9590
|
-
|
|
9042
|
+
const epoch = requestMacroRegistry.getEpoch();
|
|
9043
|
+
if (epoch !== macroProtoEpoch) {
|
|
9044
|
+
macroProtoCache = new WeakMap;
|
|
9045
|
+
macroProtoEpoch = epoch;
|
|
9046
|
+
}
|
|
9047
|
+
let base = Object.getPrototypeOf(request2) ?? Object.prototype;
|
|
9048
|
+
if (base[MACRO_PROTO]) {
|
|
9049
|
+
if (macroProtoCache.get(base[MACRO_BASE]) === base) {
|
|
9050
|
+
return request2;
|
|
9051
|
+
}
|
|
9052
|
+
base = base[MACRO_BASE];
|
|
9053
|
+
}
|
|
9054
|
+
let proto = macroProtoCache.get(base);
|
|
9055
|
+
if (!proto) {
|
|
9056
|
+
proto = Object.create(base);
|
|
9057
|
+
for (const macro of requestMacroRegistry.all()) {
|
|
9058
|
+
if (macro.accessor) {
|
|
9059
|
+
Object.defineProperty(proto, macro.name, {
|
|
9060
|
+
get: macro.handler,
|
|
9061
|
+
set(value) {
|
|
9062
|
+
Object.defineProperty(this, macro.name, {
|
|
9063
|
+
value,
|
|
9064
|
+
writable: true,
|
|
9065
|
+
configurable: true,
|
|
9066
|
+
enumerable: true
|
|
9067
|
+
});
|
|
9068
|
+
},
|
|
9069
|
+
configurable: true
|
|
9070
|
+
});
|
|
9071
|
+
} else {
|
|
9072
|
+
proto[macro.name] = macro.handler;
|
|
9073
|
+
}
|
|
9074
|
+
}
|
|
9075
|
+
Object.defineProperty(proto, MACRO_PROTO, { value: true });
|
|
9076
|
+
Object.defineProperty(proto, MACRO_BASE, { value: base });
|
|
9077
|
+
macroProtoCache.set(base, proto);
|
|
9078
|
+
}
|
|
9079
|
+
Object.setPrototypeOf(request2, proto);
|
|
9591
9080
|
return request2;
|
|
9592
9081
|
}
|
|
9593
9082
|
static hasMacro(name) {
|
|
@@ -9617,14 +9106,10 @@ var BuiltInRequestMacros = {
|
|
|
9617
9106
|
return this.headers.get("x-pjax") === "true";
|
|
9618
9107
|
},
|
|
9619
9108
|
isMobile() {
|
|
9620
|
-
|
|
9621
|
-
const mobileRegex = /Mobile|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
|
|
9622
|
-
return mobileRegex.test(userAgent);
|
|
9109
|
+
return MOBILE_REGEX.test(this.headers.get("user-agent") || "");
|
|
9623
9110
|
},
|
|
9624
9111
|
isBot() {
|
|
9625
|
-
|
|
9626
|
-
const botRegex = /bot|crawler|spider|crawling/i;
|
|
9627
|
-
return botRegex.test(userAgent);
|
|
9112
|
+
return BOT_REGEX.test(this.headers.get("user-agent") || "");
|
|
9628
9113
|
},
|
|
9629
9114
|
isSecure() {
|
|
9630
9115
|
return this.url.startsWith("https://") || this.headers.get("x-forwarded-proto") === "https" || this.headers.get("x-forwarded-ssl") === "on";
|
|
@@ -9652,6 +9137,17 @@ var BuiltInRequestMacros = {
|
|
|
9652
9137
|
}
|
|
9653
9138
|
return null;
|
|
9654
9139
|
},
|
|
9140
|
+
async rawBody() {
|
|
9141
|
+
if (typeof this._rawBody === "string")
|
|
9142
|
+
return this._rawBody;
|
|
9143
|
+
try {
|
|
9144
|
+
const text = await this.clone().text();
|
|
9145
|
+
this._rawBody = text;
|
|
9146
|
+
return text;
|
|
9147
|
+
} catch {
|
|
9148
|
+
return "";
|
|
9149
|
+
}
|
|
9150
|
+
},
|
|
9655
9151
|
basicAuth() {
|
|
9656
9152
|
const auth2 = this.headers.get("authorization");
|
|
9657
9153
|
if (auth2 && auth2.startsWith("Basic ")) {
|
|
@@ -9756,30 +9252,11 @@ var BuiltInRequestMacros = {
|
|
|
9756
9252
|
return this.params?.[key] || defaultValue;
|
|
9757
9253
|
},
|
|
9758
9254
|
cookie(name, defaultValue) {
|
|
9759
|
-
const
|
|
9760
|
-
|
|
9761
|
-
return defaultValue || null;
|
|
9762
|
-
const cookieArray = cookies.split(";");
|
|
9763
|
-
for (const cookie of cookieArray) {
|
|
9764
|
-
const [key, value] = cookie.trim().split("=");
|
|
9765
|
-
if (key === name) {
|
|
9766
|
-
return decodeURIComponent(value);
|
|
9767
|
-
}
|
|
9768
|
-
}
|
|
9769
|
-
return defaultValue || null;
|
|
9255
|
+
const value = getParsedCookies(this)[name];
|
|
9256
|
+
return value !== undefined ? value : defaultValue || null;
|
|
9770
9257
|
},
|
|
9771
9258
|
cookies() {
|
|
9772
|
-
|
|
9773
|
-
const cookieHeader = this.headers.get("cookie");
|
|
9774
|
-
if (cookieHeader) {
|
|
9775
|
-
cookieHeader.split(";").forEach((cookie) => {
|
|
9776
|
-
const [key, value] = cookie.trim().split("=");
|
|
9777
|
-
if (key && value) {
|
|
9778
|
-
cookies[key] = decodeURIComponent(value);
|
|
9779
|
-
}
|
|
9780
|
-
});
|
|
9781
|
-
}
|
|
9782
|
-
return cookies;
|
|
9259
|
+
return { ...getParsedCookies(this) };
|
|
9783
9260
|
},
|
|
9784
9261
|
file(name) {
|
|
9785
9262
|
return this.files?.find((f) => f.fieldName === name);
|
|
@@ -9788,26 +9265,33 @@ var BuiltInRequestMacros = {
|
|
|
9788
9265
|
return !!this.files?.find((f) => f.fieldName === name);
|
|
9789
9266
|
},
|
|
9790
9267
|
path() {
|
|
9791
|
-
return
|
|
9268
|
+
return getParsedURL(this).pathname;
|
|
9792
9269
|
},
|
|
9793
9270
|
fullUrl() {
|
|
9794
9271
|
return this.url;
|
|
9795
9272
|
},
|
|
9796
9273
|
root() {
|
|
9797
|
-
const url2 =
|
|
9274
|
+
const url2 = getParsedURL(this);
|
|
9798
9275
|
return `${url2.protocol}//${url2.host}`;
|
|
9799
9276
|
},
|
|
9800
9277
|
is(pattern) {
|
|
9801
9278
|
const path = this.path();
|
|
9802
|
-
|
|
9803
|
-
|
|
9279
|
+
let regex = GLOB_PATTERN_CACHE.get(pattern);
|
|
9280
|
+
if (!regex) {
|
|
9281
|
+
const regexPattern = pattern.replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
9282
|
+
regex = new RegExp(`^${regexPattern}$`);
|
|
9283
|
+
if (GLOB_PATTERN_CACHE.size >= 1000) {
|
|
9284
|
+
GLOB_PATTERN_CACHE.clear();
|
|
9285
|
+
}
|
|
9286
|
+
GLOB_PATTERN_CACHE.set(pattern, regex);
|
|
9287
|
+
}
|
|
9804
9288
|
return regex.test(path);
|
|
9805
9289
|
},
|
|
9806
9290
|
route() {
|
|
9807
9291
|
return this.routeName || null;
|
|
9808
9292
|
},
|
|
9809
9293
|
fingerprint() {
|
|
9810
|
-
const url2 =
|
|
9294
|
+
const url2 = getParsedURL(this);
|
|
9811
9295
|
const data = {
|
|
9812
9296
|
method: this.method,
|
|
9813
9297
|
path: url2.pathname,
|
|
@@ -9863,6 +9347,51 @@ function registerBuiltInRequestMacros() {
|
|
|
9863
9347
|
registerBuiltInRequestMacros();
|
|
9864
9348
|
|
|
9865
9349
|
// src/router/server.ts
|
|
9350
|
+
if (!RequestWithMacros.hasMacro("getParam")) {
|
|
9351
|
+
RequestWithMacros.macro("getParam", function(name, defaultValue) {
|
|
9352
|
+
const value = this.params?.[name];
|
|
9353
|
+
return value !== undefined ? value : defaultValue;
|
|
9354
|
+
});
|
|
9355
|
+
}
|
|
9356
|
+
if (!RequestWithMacros.hasMacro("get")) {
|
|
9357
|
+
RequestWithMacros.macro("get", function(key, defaultValue) {
|
|
9358
|
+
const value = getParsedURL(this).searchParams.get(key);
|
|
9359
|
+
return value !== null && value !== undefined ? value : defaultValue;
|
|
9360
|
+
});
|
|
9361
|
+
}
|
|
9362
|
+
RequestWithMacros.macroAccessor("cookies", function() {
|
|
9363
|
+
const req = this;
|
|
9364
|
+
const cookies = () => ({ ...getParsedCookies(req) });
|
|
9365
|
+
for (const [key, value] of Object.entries(getParsedCookies(req))) {
|
|
9366
|
+
Object.defineProperty(cookies, key, {
|
|
9367
|
+
value,
|
|
9368
|
+
writable: true,
|
|
9369
|
+
enumerable: true,
|
|
9370
|
+
configurable: true
|
|
9371
|
+
});
|
|
9372
|
+
}
|
|
9373
|
+
cookies.get = (name) => getParsedCookies(req)[name];
|
|
9374
|
+
cookies.set = (name, value, options = {}) => {
|
|
9375
|
+
if (!req._cookiesToSet) {
|
|
9376
|
+
req._cookiesToSet = [];
|
|
9377
|
+
}
|
|
9378
|
+
req._cookiesToSet.push({ name, value, options });
|
|
9379
|
+
};
|
|
9380
|
+
cookies.delete = (name, options = {}) => {
|
|
9381
|
+
if (!req._cookiesToDelete) {
|
|
9382
|
+
req._cookiesToDelete = [];
|
|
9383
|
+
}
|
|
9384
|
+
req._cookiesToDelete.push({ name, options });
|
|
9385
|
+
};
|
|
9386
|
+
cookies.getAll = () => ({ ...getParsedCookies(req) });
|
|
9387
|
+
Object.defineProperty(req, "cookies", {
|
|
9388
|
+
value: cookies,
|
|
9389
|
+
writable: true,
|
|
9390
|
+
configurable: true,
|
|
9391
|
+
enumerable: true
|
|
9392
|
+
});
|
|
9393
|
+
return cookies;
|
|
9394
|
+
});
|
|
9866
9395
|
function registerServerHandling(RouterClass) {
|
|
9867
9396
|
Object.defineProperties(RouterClass.prototype, {
|
|
9868
9397
|
serve: {
|
|
@@ -9894,6 +9423,13 @@ function registerServerHandling(RouterClass) {
|
|
|
9894
9423
|
if (options.development !== undefined) {
|
|
9895
9424
|
serverOptions.development = options.development;
|
|
9896
9425
|
}
|
|
9426
|
+
this._nativeRoutesEnabled = options.nativeRoutes === true;
|
|
9427
|
+
if (this._nativeRoutesEnabled) {
|
|
9428
|
+
const nativeRoutes = this._buildNativeRoutes();
|
|
9429
|
+
if (nativeRoutes) {
|
|
9430
|
+
serverOptions.routes = nativeRoutes;
|
|
9431
|
+
}
|
|
9432
|
+
}
|
|
9897
9433
|
if (this.wsConfig) {
|
|
9898
9434
|
serverOptions.websocket = this.wsConfig;
|
|
9899
9435
|
}
|
|
@@ -9919,12 +9455,19 @@ function registerServerHandling(RouterClass) {
|
|
|
9919
9455
|
const port = this.serverInstance.port;
|
|
9920
9456
|
const hostname = this.serverInstance.hostname;
|
|
9921
9457
|
this.serverInstance.stop();
|
|
9922
|
-
|
|
9458
|
+
const reloadOptions = {
|
|
9923
9459
|
port,
|
|
9924
9460
|
hostname,
|
|
9925
9461
|
fetch: this.handleRequest.bind(this),
|
|
9926
9462
|
websocket: this.wsConfig || undefined
|
|
9927
|
-
}
|
|
9463
|
+
};
|
|
9464
|
+
if (this._nativeRoutesEnabled) {
|
|
9465
|
+
const nativeRoutes = this._buildNativeRoutes();
|
|
9466
|
+
if (nativeRoutes) {
|
|
9467
|
+
reloadOptions.routes = nativeRoutes;
|
|
9468
|
+
}
|
|
9469
|
+
}
|
|
9470
|
+
this.serverInstance = Bun.serve(reloadOptions);
|
|
9928
9471
|
if (this.config.verbose) {
|
|
9929
9472
|
console.log(`\uD83D\uDD04 Server reloaded at http://${hostname}:${port}`);
|
|
9930
9473
|
}
|
|
@@ -9940,46 +9483,177 @@ function registerServerHandling(RouterClass) {
|
|
|
9940
9483
|
writable: true,
|
|
9941
9484
|
configurable: true
|
|
9942
9485
|
},
|
|
9486
|
+
_dispatchMatchedRoute: {
|
|
9487
|
+
async value(matchedRoute, req, params) {
|
|
9488
|
+
const enhancedReq = this.enhanceRequest(req, params);
|
|
9489
|
+
setCurrentRequest(enhancedReq);
|
|
9490
|
+
enhancedReq.route = matchedRoute;
|
|
9491
|
+
const route = matchedRoute;
|
|
9492
|
+
const epoch = this._mwEpoch || 0;
|
|
9493
|
+
const routeMwLen = route.middleware ? route.middleware.length : 0;
|
|
9494
|
+
let chain = route._compiledChain;
|
|
9495
|
+
if (!chain || route._chainEpoch !== epoch || route._chainMwLen !== routeMwLen) {
|
|
9496
|
+
const invoke = createHandlerInvoker(route.handler, this.config);
|
|
9497
|
+
if (routeMwLen === 0 && this.globalMiddleware.length === 0) {
|
|
9498
|
+
chain = invoke;
|
|
9499
|
+
} else {
|
|
9500
|
+
const middlewareStack = routeMwLen > 0 ? [...this.globalMiddleware, ...route.middleware] : [...this.globalMiddleware];
|
|
9501
|
+
middlewareStack.push((handlerReq, _next) => invoke(handlerReq));
|
|
9502
|
+
chain = this.buildMiddlewareChain(middlewareStack);
|
|
9503
|
+
}
|
|
9504
|
+
route._compiledChain = chain;
|
|
9505
|
+
route._chainEpoch = epoch;
|
|
9506
|
+
route._chainMwLen = routeMwLen;
|
|
9507
|
+
}
|
|
9508
|
+
let response2;
|
|
9509
|
+
try {
|
|
9510
|
+
response2 = await chain(enhancedReq);
|
|
9511
|
+
} catch (error) {
|
|
9512
|
+
if (this.errorHandler) {
|
|
9513
|
+
response2 = await this.errorHandler(error);
|
|
9514
|
+
} else {
|
|
9515
|
+
throw error;
|
|
9516
|
+
}
|
|
9517
|
+
}
|
|
9518
|
+
if (response2) {
|
|
9519
|
+
return this.applyModifiedCookies(response2, enhancedReq);
|
|
9520
|
+
}
|
|
9521
|
+
return new Response("No response from middleware chain", { status: 500 });
|
|
9522
|
+
},
|
|
9523
|
+
writable: true,
|
|
9524
|
+
configurable: true
|
|
9525
|
+
},
|
|
9526
|
+
_buildNativeRoutes: {
|
|
9527
|
+
value() {
|
|
9528
|
+
const NATIVE_METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]);
|
|
9529
|
+
const convertPath = (path) => {
|
|
9530
|
+
if (path === "*") {
|
|
9531
|
+
return "/*";
|
|
9532
|
+
}
|
|
9533
|
+
const segments = path.split("/");
|
|
9534
|
+
const converted = [];
|
|
9535
|
+
for (let i = 0;i < segments.length; i++) {
|
|
9536
|
+
const segment = segments[i];
|
|
9537
|
+
if (segment === "") {
|
|
9538
|
+
converted.push(segment);
|
|
9539
|
+
continue;
|
|
9540
|
+
}
|
|
9541
|
+
if (segment === "*") {
|
|
9542
|
+
return i === segments.length - 1 ? [...converted, "*"].join("/") : null;
|
|
9543
|
+
}
|
|
9544
|
+
const paramMatch = segment.match(/^\{([A-Z_$][\w$]*)\}$/i);
|
|
9545
|
+
if (paramMatch) {
|
|
9546
|
+
converted.push(`:${paramMatch[1]}`);
|
|
9547
|
+
continue;
|
|
9548
|
+
}
|
|
9549
|
+
if (segment.includes("{") || segment.includes(":") || segment.includes("*")) {
|
|
9550
|
+
return null;
|
|
9551
|
+
}
|
|
9552
|
+
converted.push(segment);
|
|
9553
|
+
}
|
|
9554
|
+
return converted.join("/");
|
|
9555
|
+
};
|
|
9556
|
+
const self = this;
|
|
9557
|
+
const wrapRoute = (route, isWildcard) => {
|
|
9558
|
+
return (req) => {
|
|
9559
|
+
return runWithRequest(req, async () => {
|
|
9560
|
+
try {
|
|
9561
|
+
let params = req.params ?? {};
|
|
9562
|
+
if (isWildcard) {
|
|
9563
|
+
const pathname = new URL(req.url).pathname;
|
|
9564
|
+
const basePath = route.path === "*" ? "/" : route.path.slice(0, -1);
|
|
9565
|
+
params = { ...params, wildcard: pathname.slice(basePath.length) };
|
|
9566
|
+
}
|
|
9567
|
+
return await self._dispatchMatchedRoute(route, req, params);
|
|
9568
|
+
} catch (error) {
|
|
9569
|
+
console.error("Error handling request:", error);
|
|
9570
|
+
if (self.errorHandler) {
|
|
9571
|
+
return self.errorHandler(error);
|
|
9572
|
+
}
|
|
9573
|
+
return new Response(JSON.stringify({
|
|
9574
|
+
success: false,
|
|
9575
|
+
message: "Internal Server Error",
|
|
9576
|
+
error: error instanceof Error ? error.message : String(error)
|
|
9577
|
+
}), {
|
|
9578
|
+
status: 500,
|
|
9579
|
+
headers: {
|
|
9580
|
+
"Content-Type": "application/json",
|
|
9581
|
+
"Access-Control-Allow-Origin": "*",
|
|
9582
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
9583
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With, Accept, Origin"
|
|
9584
|
+
}
|
|
9585
|
+
});
|
|
9586
|
+
}
|
|
9587
|
+
});
|
|
9588
|
+
};
|
|
9589
|
+
};
|
|
9590
|
+
const natives = {};
|
|
9591
|
+
const claimedShapes = new Map;
|
|
9592
|
+
for (const route of this.routes) {
|
|
9593
|
+
if (!NATIVE_METHODS.has(route.method)) {
|
|
9594
|
+
continue;
|
|
9595
|
+
}
|
|
9596
|
+
if (route.nativeDispatch === false) {
|
|
9597
|
+
continue;
|
|
9598
|
+
}
|
|
9599
|
+
if (route.constraints && Object.keys(route.constraints).length > 0) {
|
|
9600
|
+
continue;
|
|
9601
|
+
}
|
|
9602
|
+
if (route.handler instanceof Response) {
|
|
9603
|
+
continue;
|
|
9604
|
+
}
|
|
9605
|
+
const bunPath = convertPath(route.path);
|
|
9606
|
+
if (!bunPath) {
|
|
9607
|
+
continue;
|
|
9608
|
+
}
|
|
9609
|
+
const shape = bunPath.replace(/:[^/]+/g, ":p");
|
|
9610
|
+
const claimedBy = claimedShapes.get(shape);
|
|
9611
|
+
if (claimedBy && claimedBy !== bunPath) {
|
|
9612
|
+
continue;
|
|
9613
|
+
}
|
|
9614
|
+
claimedShapes.set(shape, bunPath);
|
|
9615
|
+
const entry = natives[bunPath] ??= {};
|
|
9616
|
+
if (entry[route.method]) {
|
|
9617
|
+
continue;
|
|
9618
|
+
}
|
|
9619
|
+
entry[route.method] = wrapRoute(route, route.path.endsWith("*"));
|
|
9620
|
+
}
|
|
9621
|
+
for (const entry of Object.values(natives)) {
|
|
9622
|
+
if (entry.GET && !entry.HEAD) {
|
|
9623
|
+
entry.HEAD = entry.GET;
|
|
9624
|
+
}
|
|
9625
|
+
}
|
|
9626
|
+
return Object.keys(natives).length > 0 ? natives : null;
|
|
9627
|
+
},
|
|
9628
|
+
writable: true,
|
|
9629
|
+
configurable: true
|
|
9630
|
+
},
|
|
9943
9631
|
handleRequestImpl: {
|
|
9944
9632
|
async value(req) {
|
|
9945
9633
|
try {
|
|
9946
9634
|
const url2 = new URL(req.url);
|
|
9947
|
-
|
|
9948
|
-
const hostname2 = url2.hostname || req.headers.get("host")?.split(":")[0] || "localhost";
|
|
9949
|
-
const optionsMatch = this.matchRoute(url2.pathname, "OPTIONS", hostname2);
|
|
9950
|
-
if (!optionsMatch) {
|
|
9951
|
-
return new Response(null, {
|
|
9952
|
-
status: 204,
|
|
9953
|
-
headers: {
|
|
9954
|
-
"Access-Control-Allow-Origin": "*",
|
|
9955
|
-
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
9956
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With, Accept, Origin",
|
|
9957
|
-
"Access-Control-Max-Age": "86400",
|
|
9958
|
-
"Access-Control-Allow-Credentials": "true"
|
|
9959
|
-
}
|
|
9960
|
-
});
|
|
9961
|
-
}
|
|
9962
|
-
}
|
|
9635
|
+
req._parsedURL = url2;
|
|
9963
9636
|
const hostname = url2.hostname || req.headers.get("host")?.split(":")[0] || "localhost";
|
|
9964
9637
|
const match = this.matchRoute(url2.pathname, req.method, hostname);
|
|
9965
|
-
|
|
9966
|
-
|
|
9967
|
-
|
|
9968
|
-
|
|
9969
|
-
|
|
9970
|
-
|
|
9971
|
-
|
|
9972
|
-
}
|
|
9973
|
-
const routeHandlerMiddleware = async (req2, _next) => {
|
|
9974
|
-
return await this.resolveHandler(match.route.handler, req2);
|
|
9638
|
+
if (req.method === "OPTIONS" && !match) {
|
|
9639
|
+
const origin = req.headers.get("origin");
|
|
9640
|
+
const preflightHeaders = {
|
|
9641
|
+
"Access-Control-Allow-Origin": origin || "*",
|
|
9642
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
9643
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With, Accept, Origin",
|
|
9644
|
+
"Access-Control-Max-Age": "86400"
|
|
9975
9645
|
};
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
return this.applyModifiedCookies(response2, enhancedReq);
|
|
9646
|
+
if (origin) {
|
|
9647
|
+
preflightHeaders["Access-Control-Allow-Credentials"] = "true";
|
|
9648
|
+
preflightHeaders.Vary = "Origin";
|
|
9980
9649
|
}
|
|
9981
|
-
return new Response(
|
|
9650
|
+
return new Response(null, { status: 204, headers: preflightHeaders });
|
|
9651
|
+
}
|
|
9652
|
+
if (match) {
|
|
9653
|
+
return await this._dispatchMatchedRoute(match.route, req, match.params);
|
|
9982
9654
|
}
|
|
9655
|
+
const enhancedReq = this.enhanceRequest(req, {});
|
|
9656
|
+
setCurrentRequest(enhancedReq);
|
|
9983
9657
|
const allowedMethods = this.getAllowedMethods(url2.pathname, hostname);
|
|
9984
9658
|
const corsHeaders = {
|
|
9985
9659
|
"Content-Type": "application/json",
|
|
@@ -10052,63 +9726,18 @@ function registerServerHandling(RouterClass) {
|
|
|
10052
9726
|
},
|
|
10053
9727
|
enhanceRequest: {
|
|
10054
9728
|
value(req, params = {}) {
|
|
10055
|
-
|
|
10056
|
-
|
|
10057
|
-
|
|
10058
|
-
|
|
10059
|
-
|
|
10060
|
-
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
|
|
10065
|
-
parsedCookies[name] = decodeURIComponent(value);
|
|
10066
|
-
}
|
|
10067
|
-
});
|
|
10068
|
-
}
|
|
10069
|
-
return parsedCookies;
|
|
10070
|
-
};
|
|
10071
|
-
const cookies = {
|
|
10072
|
-
get: (name) => getCookies()[name],
|
|
10073
|
-
set: (name, value, options = {}) => {
|
|
10074
|
-
const enhancedRequest = req;
|
|
10075
|
-
if (!enhancedRequest._cookiesToSet) {
|
|
10076
|
-
enhancedRequest._cookiesToSet = [];
|
|
10077
|
-
}
|
|
10078
|
-
enhancedRequest._cookiesToSet.push({ name, value, options });
|
|
10079
|
-
},
|
|
10080
|
-
delete: (name, options = {}) => {
|
|
10081
|
-
const enhancedRequest = req;
|
|
10082
|
-
if (!enhancedRequest._cookiesToDelete) {
|
|
10083
|
-
enhancedRequest._cookiesToDelete = [];
|
|
10084
|
-
}
|
|
10085
|
-
enhancedRequest._cookiesToDelete.push({ name, options });
|
|
10086
|
-
},
|
|
10087
|
-
getAll: () => ({ ...getCookies() })
|
|
10088
|
-
};
|
|
10089
|
-
const enhancedReq = Object.assign(req, {
|
|
10090
|
-
params,
|
|
10091
|
-
cookies: getCookies(),
|
|
10092
|
-
_cookiesToSet: [],
|
|
10093
|
-
_cookiesToDelete: []
|
|
10094
|
-
});
|
|
10095
|
-
RequestWithMacros.applyMacros(enhancedReq);
|
|
10096
|
-
enhancedReq.getParam = (name, defaultValue) => {
|
|
10097
|
-
const value = params?.[name];
|
|
10098
|
-
return value !== undefined ? value : defaultValue;
|
|
10099
|
-
};
|
|
10100
|
-
enhancedReq.cookie = (name, defaultValue) => {
|
|
10101
|
-
const value = getCookies()[name];
|
|
10102
|
-
return value !== undefined ? value : defaultValue ?? null;
|
|
10103
|
-
};
|
|
10104
|
-
if (typeof enhancedReq.get !== "function") {
|
|
10105
|
-
enhancedReq.get = (key, defaultValue) => {
|
|
10106
|
-
const url2 = new URL(req.url);
|
|
10107
|
-
const value = url2.searchParams.get(key);
|
|
10108
|
-
return value !== null && value !== undefined ? value : defaultValue;
|
|
10109
|
-
};
|
|
9729
|
+
const enhancedReq = req;
|
|
9730
|
+
if ("params" in req) {
|
|
9731
|
+
Object.defineProperty(enhancedReq, "params", {
|
|
9732
|
+
value: params,
|
|
9733
|
+
writable: true,
|
|
9734
|
+
configurable: true,
|
|
9735
|
+
enumerable: true
|
|
9736
|
+
});
|
|
9737
|
+
} else {
|
|
9738
|
+
enhancedReq.params = params;
|
|
10110
9739
|
}
|
|
10111
|
-
|
|
9740
|
+
RequestWithMacros.applyMacros(enhancedReq);
|
|
10112
9741
|
return enhancedReq;
|
|
10113
9742
|
},
|
|
10114
9743
|
writable: true,
|
|
@@ -10116,6 +9745,11 @@ function registerServerHandling(RouterClass) {
|
|
|
10116
9745
|
},
|
|
10117
9746
|
applyModifiedCookies: {
|
|
10118
9747
|
value(response2, req) {
|
|
9748
|
+
const hasSet = req._cookiesToSet && req._cookiesToSet.length > 0;
|
|
9749
|
+
const hasDelete = req._cookiesToDelete && req._cookiesToDelete.length > 0;
|
|
9750
|
+
if (!hasSet && !hasDelete) {
|
|
9751
|
+
return response2;
|
|
9752
|
+
}
|
|
10119
9753
|
const newResponse = new Response(response2.body, {
|
|
10120
9754
|
status: response2.status,
|
|
10121
9755
|
statusText: response2.statusText,
|
|
@@ -10665,6 +10299,7 @@ function registerBuiltInResponseMacros() {
|
|
|
10665
10299
|
registerBuiltInResponseMacros();
|
|
10666
10300
|
|
|
10667
10301
|
// src/validation/validator.ts
|
|
10302
|
+
var EMAIL_REGEX = /^[A-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[A-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@[A-Z0-9](?:[A-Z0-9-]*[A-Z0-9])?(?:\.[A-Z0-9](?:[A-Z0-9-]*[A-Z0-9])?)*\.[A-Z]{2,}$/i;
|
|
10668
10303
|
var BuiltInRules = {
|
|
10669
10304
|
required: {
|
|
10670
10305
|
name: "required",
|
|
@@ -10703,10 +10338,7 @@ var BuiltInRules = {
|
|
|
10703
10338
|
},
|
|
10704
10339
|
email: {
|
|
10705
10340
|
name: "email",
|
|
10706
|
-
validate: (value) =>
|
|
10707
|
-
const emailRegex = /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/;
|
|
10708
|
-
return typeof value === "string" && emailRegex.test(value);
|
|
10709
|
-
},
|
|
10341
|
+
validate: (value) => typeof value === "string" && EMAIL_REGEX.test(value),
|
|
10710
10342
|
message: "The :field field must be a valid email address."
|
|
10711
10343
|
},
|
|
10712
10344
|
url: {
|
|
@@ -10876,6 +10508,25 @@ var BuiltInRules = {
|
|
|
10876
10508
|
message: "The :field field must be an object."
|
|
10877
10509
|
}
|
|
10878
10510
|
};
|
|
10511
|
+
var DatabaseRules = {
|
|
10512
|
+
unique: {
|
|
10513
|
+
name: "unique",
|
|
10514
|
+
validate: async (_value, _parameters) => {
|
|
10515
|
+
console.warn('Database validation rule "unique" requires external implementation');
|
|
10516
|
+
return true;
|
|
10517
|
+
},
|
|
10518
|
+
message: "The :field field must be unique."
|
|
10519
|
+
},
|
|
10520
|
+
exists: {
|
|
10521
|
+
name: "exists",
|
|
10522
|
+
validate: async (_value, _parameters) => {
|
|
10523
|
+
console.warn('Database validation rule "exists" requires external implementation');
|
|
10524
|
+
return true;
|
|
10525
|
+
},
|
|
10526
|
+
message: "The selected :field is invalid."
|
|
10527
|
+
}
|
|
10528
|
+
};
|
|
10529
|
+
|
|
10879
10530
|
class Validator {
|
|
10880
10531
|
rules = {};
|
|
10881
10532
|
config;
|
|
@@ -10979,6 +10630,153 @@ function createValidationMiddleware(rules, config2) {
|
|
|
10979
10630
|
};
|
|
10980
10631
|
}
|
|
10981
10632
|
|
|
10633
|
+
class ValidationRuleBuilder {
|
|
10634
|
+
rules = [];
|
|
10635
|
+
required() {
|
|
10636
|
+
this.rules.push("required");
|
|
10637
|
+
return this;
|
|
10638
|
+
}
|
|
10639
|
+
string() {
|
|
10640
|
+
this.rules.push("string");
|
|
10641
|
+
return this;
|
|
10642
|
+
}
|
|
10643
|
+
number() {
|
|
10644
|
+
this.rules.push("number");
|
|
10645
|
+
return this;
|
|
10646
|
+
}
|
|
10647
|
+
integer() {
|
|
10648
|
+
this.rules.push("integer");
|
|
10649
|
+
return this;
|
|
10650
|
+
}
|
|
10651
|
+
boolean() {
|
|
10652
|
+
this.rules.push("boolean");
|
|
10653
|
+
return this;
|
|
10654
|
+
}
|
|
10655
|
+
email() {
|
|
10656
|
+
this.rules.push("email");
|
|
10657
|
+
return this;
|
|
10658
|
+
}
|
|
10659
|
+
url() {
|
|
10660
|
+
this.rules.push("url");
|
|
10661
|
+
return this;
|
|
10662
|
+
}
|
|
10663
|
+
min(value) {
|
|
10664
|
+
this.rules.push(`min:${value}`);
|
|
10665
|
+
return this;
|
|
10666
|
+
}
|
|
10667
|
+
max(value) {
|
|
10668
|
+
this.rules.push(`max:${value}`);
|
|
10669
|
+
return this;
|
|
10670
|
+
}
|
|
10671
|
+
between(min, max) {
|
|
10672
|
+
this.rules.push(`between:${min},${max}`);
|
|
10673
|
+
return this;
|
|
10674
|
+
}
|
|
10675
|
+
in(...values) {
|
|
10676
|
+
this.rules.push(`in:${values.join(",")}`);
|
|
10677
|
+
return this;
|
|
10678
|
+
}
|
|
10679
|
+
notIn(...values) {
|
|
10680
|
+
this.rules.push(`not_in:${values.join(",")}`);
|
|
10681
|
+
return this;
|
|
10682
|
+
}
|
|
10683
|
+
regex(pattern) {
|
|
10684
|
+
this.rules.push(`regex:${pattern}`);
|
|
10685
|
+
return this;
|
|
10686
|
+
}
|
|
10687
|
+
alpha() {
|
|
10688
|
+
this.rules.push("alpha");
|
|
10689
|
+
return this;
|
|
10690
|
+
}
|
|
10691
|
+
alphaNum() {
|
|
10692
|
+
this.rules.push("alpha_num");
|
|
10693
|
+
return this;
|
|
10694
|
+
}
|
|
10695
|
+
alphaDash() {
|
|
10696
|
+
this.rules.push("alpha_dash");
|
|
10697
|
+
return this;
|
|
10698
|
+
}
|
|
10699
|
+
date() {
|
|
10700
|
+
this.rules.push("date");
|
|
10701
|
+
return this;
|
|
10702
|
+
}
|
|
10703
|
+
after(date) {
|
|
10704
|
+
this.rules.push(`after:${date}`);
|
|
10705
|
+
return this;
|
|
10706
|
+
}
|
|
10707
|
+
before(date) {
|
|
10708
|
+
this.rules.push(`before:${date}`);
|
|
10709
|
+
return this;
|
|
10710
|
+
}
|
|
10711
|
+
confirmed(field) {
|
|
10712
|
+
this.rules.push(field ? `confirmed:${field}` : "confirmed");
|
|
10713
|
+
return this;
|
|
10714
|
+
}
|
|
10715
|
+
same(field) {
|
|
10716
|
+
this.rules.push(`same:${field}`);
|
|
10717
|
+
return this;
|
|
10718
|
+
}
|
|
10719
|
+
different(field) {
|
|
10720
|
+
this.rules.push(`different:${field}`);
|
|
10721
|
+
return this;
|
|
10722
|
+
}
|
|
10723
|
+
json() {
|
|
10724
|
+
this.rules.push("json");
|
|
10725
|
+
return this;
|
|
10726
|
+
}
|
|
10727
|
+
uuid() {
|
|
10728
|
+
this.rules.push("uuid");
|
|
10729
|
+
return this;
|
|
10730
|
+
}
|
|
10731
|
+
array() {
|
|
10732
|
+
this.rules.push("array");
|
|
10733
|
+
return this;
|
|
10734
|
+
}
|
|
10735
|
+
object() {
|
|
10736
|
+
this.rules.push("object");
|
|
10737
|
+
return this;
|
|
10738
|
+
}
|
|
10739
|
+
unique(table, column) {
|
|
10740
|
+
this.rules.push(column ? `unique:${table},${column}` : `unique:${table}`);
|
|
10741
|
+
return this;
|
|
10742
|
+
}
|
|
10743
|
+
exists(table, column) {
|
|
10744
|
+
this.rules.push(column ? `exists:${table},${column}` : `exists:${table}`);
|
|
10745
|
+
return this;
|
|
10746
|
+
}
|
|
10747
|
+
custom(ruleName, ...parameters) {
|
|
10748
|
+
this.rules.push(parameters.length > 0 ? `${ruleName}:${parameters.join(",")}` : ruleName);
|
|
10749
|
+
return this;
|
|
10750
|
+
}
|
|
10751
|
+
build() {
|
|
10752
|
+
return this.rules.join("|");
|
|
10753
|
+
}
|
|
10754
|
+
}
|
|
10755
|
+
function rule() {
|
|
10756
|
+
return new ValidationRuleBuilder;
|
|
10757
|
+
}
|
|
10758
|
+
var ValidationHelpers = {
|
|
10759
|
+
rules(rules) {
|
|
10760
|
+
const result = {};
|
|
10761
|
+
for (const [field, fieldRules] of Object.entries(rules)) {
|
|
10762
|
+
result[field] = fieldRules instanceof ValidationRuleBuilder ? fieldRules.build() : fieldRules;
|
|
10763
|
+
}
|
|
10764
|
+
return result;
|
|
10765
|
+
},
|
|
10766
|
+
async validateValue(value, rules, field = "value") {
|
|
10767
|
+
const validator = new Validator;
|
|
10768
|
+
const errors = await validator.validate({ [field]: value }, { [field]: rules });
|
|
10769
|
+
return errors[field] || [];
|
|
10770
|
+
},
|
|
10771
|
+
async passes(value, rules) {
|
|
10772
|
+
const errors = await this.validateValue(value, rules);
|
|
10773
|
+
return errors.length === 0;
|
|
10774
|
+
},
|
|
10775
|
+
async fails(value, rules) {
|
|
10776
|
+
return !await this.passes(value, rules);
|
|
10777
|
+
}
|
|
10778
|
+
};
|
|
10779
|
+
|
|
10982
10780
|
// src/router/validation-integration.ts
|
|
10983
10781
|
class RouteBuilder {
|
|
10984
10782
|
method;
|
|
@@ -11015,7 +10813,7 @@ class RouteBuilder {
|
|
|
11015
10813
|
}
|
|
11016
10814
|
}
|
|
11017
10815
|
|
|
11018
|
-
class
|
|
10816
|
+
class ValidationFluentRouter {
|
|
11019
10817
|
routes = [];
|
|
11020
10818
|
get(path, handler) {
|
|
11021
10819
|
return new RouteBuilder("GET", path, handler);
|
|
@@ -11128,7 +10926,7 @@ var RouteHelpers = {
|
|
|
11128
10926
|
}
|
|
11129
10927
|
};
|
|
11130
10928
|
function createFluentRouter() {
|
|
11131
|
-
return new
|
|
10929
|
+
return new ValidationFluentRouter;
|
|
11132
10930
|
}
|
|
11133
10931
|
var MiddlewareHelpers = {
|
|
11134
10932
|
compose: (...middleware2) => {
|
|
@@ -13782,11 +13580,13 @@ export {
|
|
|
13782
13580
|
singleFileUpload,
|
|
13783
13581
|
signRequest,
|
|
13784
13582
|
setCurrentRequest,
|
|
13583
|
+
setContainer,
|
|
13785
13584
|
session,
|
|
13786
13585
|
securitySuite,
|
|
13787
13586
|
security,
|
|
13788
13587
|
scopedBindingHelpers,
|
|
13789
13588
|
runWithRequest,
|
|
13589
|
+
rule,
|
|
13790
13590
|
router,
|
|
13791
13591
|
routeModelHelpers,
|
|
13792
13592
|
routeCacheManager,
|
|
@@ -13825,10 +13625,12 @@ export {
|
|
|
13825
13625
|
inputValidation,
|
|
13826
13626
|
injectQueryPreservationScript,
|
|
13827
13627
|
imageUpload,
|
|
13628
|
+
globalValidator,
|
|
13828
13629
|
globalMiddlewarePipeline,
|
|
13829
13630
|
getNamedRoutes,
|
|
13830
13631
|
getNamedRoutePath,
|
|
13831
13632
|
getCurrentRequest,
|
|
13633
|
+
getContainer,
|
|
13832
13634
|
getConfig,
|
|
13833
13635
|
generateSessionId,
|
|
13834
13636
|
generateQueryPreservationScript,
|
|
@@ -13841,12 +13643,14 @@ export {
|
|
|
13841
13643
|
extractBearerToken,
|
|
13842
13644
|
extractBasicAuth,
|
|
13843
13645
|
extractApiKey,
|
|
13646
|
+
enableRequestContext,
|
|
13844
13647
|
documentUpload,
|
|
13845
13648
|
defaultQueryPreservationConfig,
|
|
13846
13649
|
defaultConfig,
|
|
13847
13650
|
ddosProtection,
|
|
13848
13651
|
csrf,
|
|
13849
13652
|
createWebSocketTester,
|
|
13653
|
+
createValidationMiddleware,
|
|
13850
13654
|
createTestResponse,
|
|
13851
13655
|
createTestClient,
|
|
13852
13656
|
createSubdomainMiddleware,
|
|
@@ -13868,6 +13672,7 @@ export {
|
|
|
13868
13672
|
createFluentRouter,
|
|
13869
13673
|
createFileUploadTester,
|
|
13870
13674
|
createEnhancementMiddleware,
|
|
13675
|
+
createContainer,
|
|
13871
13676
|
createAuthTester,
|
|
13872
13677
|
cors,
|
|
13873
13678
|
contentSecurityPolicy,
|
|
@@ -13885,7 +13690,11 @@ export {
|
|
|
13885
13690
|
applyRequestEnhancements,
|
|
13886
13691
|
apiKeyAuth,
|
|
13887
13692
|
WebSocketTester,
|
|
13693
|
+
Validator,
|
|
13694
|
+
ValidationRuleBuilder,
|
|
13888
13695
|
ValidationMiddlewareBuilder,
|
|
13696
|
+
ValidationHelpers,
|
|
13697
|
+
ValidationFluentRouter,
|
|
13889
13698
|
ThrottleFactory,
|
|
13890
13699
|
TestResponseWrapper,
|
|
13891
13700
|
TestRequestBuilder,
|
|
@@ -13951,17 +13760,24 @@ export {
|
|
|
13951
13760
|
FileSessionStore,
|
|
13952
13761
|
FileSecurity,
|
|
13953
13762
|
exports_errors as Errors,
|
|
13763
|
+
EnvironmentManager,
|
|
13954
13764
|
EnhancementPresets,
|
|
13955
13765
|
DomainMatcher,
|
|
13956
13766
|
DomainGroupBuilder,
|
|
13957
13767
|
DomainGroup,
|
|
13958
13768
|
Dependencies,
|
|
13769
|
+
DefaultServiceProviderManager,
|
|
13959
13770
|
DatabaseSessionStore,
|
|
13771
|
+
DatabaseRules,
|
|
13960
13772
|
DDoSProtection,
|
|
13961
13773
|
Csrf,
|
|
13962
13774
|
Cors,
|
|
13775
|
+
ContextualContainer,
|
|
13963
13776
|
ContentSecurityPolicy,
|
|
13777
|
+
Container,
|
|
13964
13778
|
CacheInvalidation,
|
|
13779
|
+
BuiltInRules,
|
|
13780
|
+
BindingBuilder,
|
|
13965
13781
|
AuthTester,
|
|
13966
13782
|
auth_default as Auth,
|
|
13967
13783
|
ApiKeyManager
|