@fluojs/http 1.0.0-beta.3 → 1.0.0-beta.5
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/README.ko.md +4 -1
- package/README.md +4 -1
- package/dist/adapters/binding.d.ts.map +1 -1
- package/dist/adapters/binding.js +46 -55
- package/dist/adapters/dto-binding-plan.d.ts +22 -0
- package/dist/adapters/dto-binding-plan.d.ts.map +1 -0
- package/dist/adapters/dto-binding-plan.js +86 -0
- package/dist/adapters/dto-validation-adapter.d.ts +1 -1
- package/dist/adapters/dto-validation-adapter.d.ts.map +1 -1
- package/dist/adapters/dto-validation-adapter.js +9 -15
- package/dist/dispatch/dispatch-handler-policy.d.ts +3 -1
- package/dist/dispatch/dispatch-handler-policy.d.ts.map +1 -1
- package/dist/dispatch/dispatch-handler-policy.js +8 -5
- package/dist/dispatch/dispatch-response-policy.d.ts.map +1 -1
- package/dist/dispatch/dispatch-response-policy.js +32 -0
- package/dist/dispatch/dispatcher.d.ts +6 -1
- package/dist/dispatch/dispatcher.d.ts.map +1 -1
- package/dist/dispatch/dispatcher.js +164 -22
- package/dist/dispatch/native-route-handoff.d.ts +53 -0
- package/dist/dispatch/native-route-handoff.d.ts.map +1 -0
- package/dist/dispatch/native-route-handoff.js +94 -0
- package/dist/internal.d.ts +1 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +2 -1
- package/package.json +3 -3
package/README.ko.md
CHANGED
|
@@ -115,7 +115,7 @@ stream(_input: undefined, ctx: RequestContext) {
|
|
|
115
115
|
|
|
116
116
|
## 요청 정리와 런타임 이식성
|
|
117
117
|
|
|
118
|
-
디스패처는 활성 dispatch 동안에만 `AsyncLocalStorage`로 `RequestContext`를
|
|
118
|
+
디스패처는 활성 dispatch 동안에만 `AsyncLocalStorage`로 `RequestContext`를 바인딩합니다. 요청이 controller graph, middleware, guard, interceptor, observer, DTO converter, custom binder 또는 수동 `getCurrentRequestContext()` / `assertRequestContext()` container 접근을 통해 request-scoped DI를 사용할 수 있으면, 디스패처는 요청 observer가 끝난 뒤 `finally` 경로에서 isolated request-scoped DI 컨테이너를 생성하고 dispose합니다. Singleton-only route는 `RequestContext.container`가 접근되기 전까지 이 컨테이너 lifecycle을 건너뛰어 baseline 경로의 불필요한 per-request allocation을 피하면서도, graph가 모호하거나 request-scoped이면 request-scoped provider isolation을 유지합니다. 따라서 공개 `RequestContext.container` 읽기는 request-scoped provider resolve에 항상 안전합니다. singleton-only fast path는 내부 dispatcher 최적화일 뿐, 공개 context가 root container를 노출한다는 약속이 아닙니다.
|
|
119
119
|
|
|
120
120
|
어댑터는 플랫폼이 제공한다면 `FrameworkRequest.signal`에 `AbortSignal`을 전달해야 합니다. SSE에서는 가능하면 `FrameworkResponse.stream.onClose(...)`도 노출해야 합니다. `SseResponse`는 request abort와 raw stream close를 모두 구독하고, 멱등하게 닫히며, 어느 쪽이 먼저 종료되더라도 등록한 listener를 제거합니다.
|
|
121
121
|
|
|
@@ -133,6 +133,9 @@ stream(_input: undefined, ctx: RequestContext) {
|
|
|
133
133
|
`./internal` 서브경로는 플랫폼 어댑터와 핵심 런타임에서 사용하는 저수준 유틸리티만 내보냅니다. 이들은 변경될 수 있으며 일반적인 애플리케이션 코드에서 사용해서는 안 됩니다.
|
|
134
134
|
|
|
135
135
|
- `DefaultBinder`: 런타임 부트스트랩 경로에서 사용하는 기본 DTO/요청 바인더.
|
|
136
|
+
- `bindRawRequestNativeRouteHandoff(...)` / `attachFrameworkRequestNativeRouteHandoff(...)`: public dispatcher API를 넓히지 않고 의미 보존이 가능한 native route match를 재사용하기 위한 내부 adapter/runtime 헬퍼.
|
|
137
|
+
- Native route handoff는 framework request에 붙는 시점의 method와 path를 함께 스냅샷합니다. app middleware가 handler matching 전에 둘 중 하나를 rewrite하면 dispatcher는 stale handoff를 무시하고 일반 route matching으로 fallback합니다.
|
|
138
|
+
- `isRoutePathNormalizationSensitive(path)`: duplicate slash와 trailing slash 요청을 generic dispatcher 경로에 남기기 위한 내부 guard.
|
|
136
139
|
- `resolveClientIdentity(request)`: 속도 제한과 런타임 통합에서 사용하는 보수적 클라이언트 식별 해석기.
|
|
137
140
|
|
|
138
141
|
## 관련 패키지
|
package/README.md
CHANGED
|
@@ -117,7 +117,7 @@ stream(_input: undefined, ctx: RequestContext) {
|
|
|
117
117
|
|
|
118
118
|
## Request Cleanup and Portability
|
|
119
119
|
|
|
120
|
-
The dispatcher binds `RequestContext` with `AsyncLocalStorage` for the active dispatch only and disposes
|
|
120
|
+
The dispatcher binds `RequestContext` with `AsyncLocalStorage` for the active dispatch only. When a request may use request-scoped DI through its controller graph, middleware, guards, interceptors, observers, DTO converters, a custom binder, or manual `getCurrentRequestContext()` / `assertRequestContext()` container access, the dispatcher creates and disposes an isolated request-scoped DI container from its `finally` path after request observers finish. Singleton-only routes skip that container lifecycle until `RequestContext.container` is accessed, so the baseline path avoids unnecessary per-request allocation while preserving request-scoped provider isolation whenever the graph is ambiguous or request-scoped. Public `RequestContext.container` reads are therefore always safe for resolving request-scoped providers; the singleton-only fast path is an internal dispatcher optimization, not a promise that the public context exposes the root container.
|
|
121
121
|
|
|
122
122
|
Adapters should pass an `AbortSignal` on `FrameworkRequest.signal` when the platform exposes one. For SSE, adapters should also expose `FrameworkResponse.stream.onClose(...)` when possible; `SseResponse` listens to both request abort and raw stream close, closes idempotently, and removes registered listeners when either side terminates first.
|
|
123
123
|
|
|
@@ -135,6 +135,9 @@ Adapters should pass an `AbortSignal` on `FrameworkRequest.signal` when the plat
|
|
|
135
135
|
The `./internal` subpath exports only the low-level utilities used by platform adapters and the core runtime. These are subject to change and should not be used in typical application code.
|
|
136
136
|
|
|
137
137
|
- `DefaultBinder`: Default DTO/request binder used by the runtime bootstrap path.
|
|
138
|
+
- `bindRawRequestNativeRouteHandoff(...)` / `attachFrameworkRequestNativeRouteHandoff(...)`: Internal adapter/runtime helpers for reusing semantically safe native route matches without widening the public dispatcher API.
|
|
139
|
+
- Native route handoffs snapshot the framework request method and path when attached; if app middleware rewrites either value before handler matching, the dispatcher ignores the stale handoff and falls back to normal route matching.
|
|
140
|
+
- `isRoutePathNormalizationSensitive(path)`: Internal guard for keeping duplicate-slash and trailing-slash requests on the generic dispatcher path.
|
|
138
141
|
- `resolveClientIdentity(request)`: Conservative client identity resolver used by rate limiting and other runtime integrations.
|
|
139
142
|
|
|
140
143
|
## Related Packages
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../../src/adapters/binding.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,WAAW,
|
|
1
|
+
{"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../../src/adapters/binding.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,WAAW,EAAc,MAAM,cAAc,CAAC;AAI5E,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,eAAe,EAAoB,MAAM,aAAa,CAAC;AAkDhI;;GAEG;AACH,qBAAa,gBAAiB,YAAW,SAAS;IAChD,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO;CAG3D;AAyDD;;GAEG;AACH,qBAAa,aAAc,YAAW,MAAM;IAC9B,OAAO,CAAC,QAAQ,CAAC,UAAU;gBAAV,UAAU,GAAE,SAAS,aAAa,EAAO;IAEhE,IAAI,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,OAAO,CAAC;CAuGjF"}
|
package/dist/adapters/binding.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { InvariantError } from '@fluojs/core';
|
|
2
|
-
import { getDtoBindingSchema } from '@fluojs/core/internal';
|
|
3
2
|
import { BadRequestException } from '../exceptions.js';
|
|
4
3
|
import { toInputErrorDetail } from '../input-error-detail.js';
|
|
4
|
+
import { getCompiledDtoBindingPlan } from './dto-binding-plan.js';
|
|
5
5
|
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
6
|
+
const NO_CONVERTERS = [];
|
|
6
7
|
function isPlainObject(value) {
|
|
7
8
|
if (typeof value !== 'object' || value === null) {
|
|
8
9
|
return false;
|
|
@@ -10,44 +11,6 @@ function isPlainObject(value) {
|
|
|
10
11
|
const prototype = Object.getPrototypeOf(value);
|
|
11
12
|
return prototype === Object.prototype || prototype === null;
|
|
12
13
|
}
|
|
13
|
-
function toFieldName(propertyKey) {
|
|
14
|
-
return typeof propertyKey === 'string' ? propertyKey : String(propertyKey);
|
|
15
|
-
}
|
|
16
|
-
function resolveSourceKey(propertyKey, key) {
|
|
17
|
-
return key ?? toFieldName(propertyKey);
|
|
18
|
-
}
|
|
19
|
-
function readHeader(request, key) {
|
|
20
|
-
return request.headers[key.toLowerCase()] ?? request.headers[key];
|
|
21
|
-
}
|
|
22
|
-
function readSourceValue(request, source, propertyKey, key) {
|
|
23
|
-
const resolvedKey = resolveSourceKey(propertyKey, key);
|
|
24
|
-
switch (source) {
|
|
25
|
-
case 'path':
|
|
26
|
-
return request.params[resolvedKey];
|
|
27
|
-
case 'query':
|
|
28
|
-
return request.query[resolvedKey];
|
|
29
|
-
case 'header':
|
|
30
|
-
return readHeader(request, resolvedKey);
|
|
31
|
-
case 'cookie':
|
|
32
|
-
return request.cookies[resolvedKey];
|
|
33
|
-
case 'body':
|
|
34
|
-
{
|
|
35
|
-
if (!isPlainObject(request.body)) {
|
|
36
|
-
if (request.body !== undefined && request.body !== null) {
|
|
37
|
-
throw new BadRequestException('Request body must be a plain object.', {
|
|
38
|
-
details: [toInputErrorDetail({
|
|
39
|
-
code: 'INVALID_BODY',
|
|
40
|
-
message: 'Request body must be a plain object.',
|
|
41
|
-
source: 'body'
|
|
42
|
-
})]
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
return undefined;
|
|
46
|
-
}
|
|
47
|
-
return request.body[resolvedKey];
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
14
|
function validateBodyKeys(request, bodyKeys) {
|
|
52
15
|
if (request.body === undefined || request.body === null) {
|
|
53
16
|
return;
|
|
@@ -144,42 +107,70 @@ export class DefaultBinder {
|
|
|
144
107
|
this.converters = converters;
|
|
145
108
|
}
|
|
146
109
|
async bind(dto, context) {
|
|
147
|
-
const
|
|
110
|
+
const plan = getCompiledDtoBindingPlan(dto);
|
|
111
|
+
const request = context.requestContext.request;
|
|
148
112
|
const value = new dto();
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
validateBodyKeys(context.requestContext.request, bodyKeys);
|
|
113
|
+
if (request.body !== undefined && request.body !== null) {
|
|
114
|
+
validateBodyKeys(request, plan.bodyKeys);
|
|
115
|
+
}
|
|
153
116
|
const details = [];
|
|
154
|
-
|
|
155
|
-
|
|
117
|
+
if (this.converters.length === 0 && !plan.hasFieldConverters) {
|
|
118
|
+
for (const entry of plan.entries) {
|
|
119
|
+
const rawValue = entry.read(request);
|
|
120
|
+
if (rawValue === undefined) {
|
|
121
|
+
if (entry.optional) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
details.push(toInputErrorDetail({
|
|
125
|
+
code: 'MISSING_FIELD',
|
|
126
|
+
field: entry.fieldName,
|
|
127
|
+
message: `Missing required ${entry.source} field ${entry.sourceKey}.`,
|
|
128
|
+
source: entry.source
|
|
129
|
+
}));
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
value[entry.propertyKey] = rawValue;
|
|
133
|
+
}
|
|
134
|
+
if (details.length > 0) {
|
|
135
|
+
throw new BadRequestException('Request binding failed.', {
|
|
136
|
+
details
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
const converterCache = new Map();
|
|
142
|
+
const globalConverters = this.converters.length === 0 ? NO_CONVERTERS : (await Promise.all(this.converters.map(converter => resolveConverter(converter, context, converterCache)))).filter(converter => Boolean(converter));
|
|
143
|
+
for (const entry of plan.entries) {
|
|
144
|
+
const rawValue = entry.read(request);
|
|
156
145
|
if (rawValue === undefined) {
|
|
157
|
-
if (entry.
|
|
146
|
+
if (entry.optional) {
|
|
158
147
|
continue;
|
|
159
148
|
}
|
|
160
149
|
details.push(toInputErrorDetail({
|
|
161
150
|
code: 'MISSING_FIELD',
|
|
162
|
-
field:
|
|
163
|
-
message: `Missing required ${entry.
|
|
164
|
-
source: entry.
|
|
151
|
+
field: entry.fieldName,
|
|
152
|
+
message: `Missing required ${entry.source} field ${entry.sourceKey}.`,
|
|
153
|
+
source: entry.source
|
|
165
154
|
}));
|
|
166
155
|
continue;
|
|
167
156
|
}
|
|
168
157
|
const target = {
|
|
169
158
|
dto,
|
|
170
159
|
handler: context.handler,
|
|
171
|
-
key:
|
|
160
|
+
key: entry.sourceKey,
|
|
172
161
|
propertyKey: entry.propertyKey,
|
|
173
162
|
requestContext: context.requestContext,
|
|
174
|
-
source: entry.
|
|
163
|
+
source: entry.source
|
|
175
164
|
};
|
|
176
165
|
let convertedValue = rawValue;
|
|
177
166
|
for (const converter of globalConverters) {
|
|
178
167
|
convertedValue = await converter.convert(convertedValue, target);
|
|
179
168
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
169
|
+
if (entry.converter !== undefined) {
|
|
170
|
+
const fieldConverter = await resolveConverter(entry.converter, context, converterCache);
|
|
171
|
+
if (fieldConverter) {
|
|
172
|
+
convertedValue = await fieldConverter.convert(convertedValue, target);
|
|
173
|
+
}
|
|
183
174
|
}
|
|
184
175
|
value[entry.propertyKey] = convertedValue;
|
|
185
176
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Constructor, type MetadataPropertyKey, type MetadataSource } from '@fluojs/core';
|
|
2
|
+
import { type DtoFieldBindingMetadata } from '@fluojs/core/internal';
|
|
3
|
+
import type { FrameworkRequest } from '../types.js';
|
|
4
|
+
export interface CompiledDtoBindingPlanEntry {
|
|
5
|
+
readonly converter?: DtoFieldBindingMetadata['converter'];
|
|
6
|
+
readonly fieldName: string;
|
|
7
|
+
readonly optional: boolean;
|
|
8
|
+
readonly propertyKey: MetadataPropertyKey;
|
|
9
|
+
readonly read: (request: FrameworkRequest) => unknown;
|
|
10
|
+
readonly source: MetadataSource;
|
|
11
|
+
readonly sourceKey: string;
|
|
12
|
+
}
|
|
13
|
+
export interface CompiledDtoBindingPlan {
|
|
14
|
+
readonly bodyKeys: ReadonlySet<string>;
|
|
15
|
+
readonly entries: readonly CompiledDtoBindingPlanEntry[];
|
|
16
|
+
readonly hasFieldConverters: boolean;
|
|
17
|
+
readonly needsValidation: boolean;
|
|
18
|
+
readonly propertyKeys: readonly MetadataPropertyKey[];
|
|
19
|
+
readonly toValidationValue: (value: unknown) => unknown;
|
|
20
|
+
}
|
|
21
|
+
export declare function getCompiledDtoBindingPlan(dto: Constructor): CompiledDtoBindingPlan;
|
|
22
|
+
//# sourceMappingURL=dto-binding-plan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dto-binding-plan.d.ts","sourceRoot":"","sources":["../../src/adapters/dto-binding-plan.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,mBAAmB,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAC/F,OAAO,EAKL,KAAK,uBAAuB,EAE7B,MAAM,uBAAuB,CAAC;AAE/B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAcpD,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,uBAAuB,CAAC,WAAW,CAAC,CAAC;IAC1D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,mBAAmB,CAAC;IAC1C,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC;IACtD,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,SAAS,2BAA2B,EAAE,CAAC;IACzD,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IACrC,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAClC,QAAQ,CAAC,YAAY,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACtD,QAAQ,CAAC,iBAAiB,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;CACzD;AAkDD,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,WAAW,GAAG,sBAAsB,CA4ClF"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { getClassValidationRules, getDtoBindingSchema, getDtoValidationSchema } from '@fluojs/core/internal';
|
|
2
|
+
function toFieldName(propertyKey) {
|
|
3
|
+
return typeof propertyKey === 'string' ? propertyKey : String(propertyKey);
|
|
4
|
+
}
|
|
5
|
+
function resolveSourceKey(propertyKey, key) {
|
|
6
|
+
return key ?? toFieldName(propertyKey);
|
|
7
|
+
}
|
|
8
|
+
function readHeader(request, key) {
|
|
9
|
+
return request.headers[key.toLowerCase()] ?? request.headers[key];
|
|
10
|
+
}
|
|
11
|
+
const dtoBindingPlanCache = new WeakMap();
|
|
12
|
+
function createSourceReader(source, sourceKey) {
|
|
13
|
+
switch (source) {
|
|
14
|
+
case 'path':
|
|
15
|
+
return request => request.params[sourceKey];
|
|
16
|
+
case 'query':
|
|
17
|
+
return request => request.query[sourceKey];
|
|
18
|
+
case 'header':
|
|
19
|
+
return request => readHeader(request, sourceKey);
|
|
20
|
+
case 'cookie':
|
|
21
|
+
return request => request.cookies[sourceKey];
|
|
22
|
+
case 'body':
|
|
23
|
+
return request => request.body?.[sourceKey];
|
|
24
|
+
default:
|
|
25
|
+
return () => undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function identityValidationValue(value) {
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function createValidationValueFilter(propertyKeys) {
|
|
32
|
+
return value => {
|
|
33
|
+
if (typeof value !== 'object' || value === null) {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
const source = value;
|
|
37
|
+
const filtered = Object.create(Object.getPrototypeOf(value));
|
|
38
|
+
for (const propertyKey of propertyKeys) {
|
|
39
|
+
if (Object.hasOwn(source, propertyKey)) {
|
|
40
|
+
filtered[propertyKey] = source[propertyKey];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return filtered;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function isDtoAwareValidationRule(rule) {
|
|
47
|
+
return rule.kind === 'custom' || rule.kind === 'validateIf';
|
|
48
|
+
}
|
|
49
|
+
export function getCompiledDtoBindingPlan(dto) {
|
|
50
|
+
const cached = dtoBindingPlanCache.get(dto);
|
|
51
|
+
if (cached) {
|
|
52
|
+
return cached;
|
|
53
|
+
}
|
|
54
|
+
const entries = getDtoBindingSchema(dto).map(entry => {
|
|
55
|
+
const sourceKey = resolveSourceKey(entry.propertyKey, entry.metadata.key);
|
|
56
|
+
return {
|
|
57
|
+
...(entry.metadata.converter === undefined ? {} : {
|
|
58
|
+
converter: entry.metadata.converter
|
|
59
|
+
}),
|
|
60
|
+
fieldName: toFieldName(entry.propertyKey),
|
|
61
|
+
optional: entry.metadata.optional === true,
|
|
62
|
+
propertyKey: entry.propertyKey,
|
|
63
|
+
read: createSourceReader(entry.metadata.source, sourceKey),
|
|
64
|
+
source: entry.metadata.source,
|
|
65
|
+
sourceKey
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
const propertyKeys = entries.map(entry => entry.propertyKey);
|
|
69
|
+
const boundPropertyKeys = new Set(propertyKeys);
|
|
70
|
+
const validationSchema = getDtoValidationSchema(dto);
|
|
71
|
+
const validationPropertyKeys = validationSchema.map(entry => entry.propertyKey);
|
|
72
|
+
const hasClassValidationRules = getClassValidationRules(dto).length > 0;
|
|
73
|
+
const hasDtoAwareValidationRules = validationSchema.some(entry => entry.rules.some(rule => isDtoAwareValidationRule(rule)));
|
|
74
|
+
const needsValidation = hasClassValidationRules || validationPropertyKeys.length > 0;
|
|
75
|
+
const requiresValidationFilter = hasClassValidationRules || hasDtoAwareValidationRules || validationPropertyKeys.some(propertyKey => !boundPropertyKeys.has(propertyKey));
|
|
76
|
+
const next = {
|
|
77
|
+
bodyKeys: new Set(entries.filter(entry => entry.source === 'body').map(entry => entry.sourceKey)),
|
|
78
|
+
entries,
|
|
79
|
+
hasFieldConverters: entries.some(entry => entry.converter !== undefined),
|
|
80
|
+
needsValidation,
|
|
81
|
+
propertyKeys,
|
|
82
|
+
toValidationValue: requiresValidationFilter ? createValidationValueFilter(propertyKeys) : identityValidationValue
|
|
83
|
+
};
|
|
84
|
+
dtoBindingPlanCache.set(dto, next);
|
|
85
|
+
return next;
|
|
86
|
+
}
|
|
@@ -6,7 +6,7 @@ import type { Validator } from '../types.js';
|
|
|
6
6
|
export declare class HttpDtoValidationAdapter implements Validator {
|
|
7
7
|
private readonly validator;
|
|
8
8
|
private throwBadRequestForValidationError;
|
|
9
|
-
private
|
|
9
|
+
private getValidationValue;
|
|
10
10
|
validate(value: unknown, target: Constructor): Promise<void>;
|
|
11
11
|
materialize<T>(value: unknown, target: Constructor<T>): Promise<T>;
|
|
12
12
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dto-validation-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/dto-validation-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"dto-validation-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/dto-validation-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAKhD,OAAO,KAAK,EAAmB,SAAS,EAAE,MAAM,aAAa,CAAC;AAG9D;;GAEG;AACH,qBAAa,wBAAyB,YAAW,SAAS;IACxD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA8B;IAExD,OAAO,CAAC,iCAAiC;IAMzC,OAAO,CAAC,kBAAkB;IAIpB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB5D,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAWzE"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { getDtoBindingSchema } from '@fluojs/core/internal';
|
|
2
1
|
import { DefaultValidator as BaseDefaultValidator, DtoValidationError } from '@fluojs/validation';
|
|
3
2
|
import { BadRequestException } from '../exceptions.js';
|
|
4
3
|
import { toInputErrorDetail } from '../input-error-detail.js';
|
|
4
|
+
import { getCompiledDtoBindingPlan } from './dto-binding-plan.js';
|
|
5
|
+
|
|
5
6
|
/**
|
|
6
7
|
* Represents the http dto validation adapter.
|
|
7
8
|
*/
|
|
@@ -12,23 +13,16 @@ export class HttpDtoValidationAdapter {
|
|
|
12
13
|
details: error.issues.map(issue => toInputErrorDetail(issue))
|
|
13
14
|
});
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return value;
|
|
18
|
-
}
|
|
19
|
-
const source = value;
|
|
20
|
-
const filtered = Object.create(Object.getPrototypeOf(value));
|
|
21
|
-
for (const binding of getDtoBindingSchema(target)) {
|
|
22
|
-
if (Object.hasOwn(source, binding.propertyKey)) {
|
|
23
|
-
filtered[binding.propertyKey] = source[binding.propertyKey];
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return filtered;
|
|
16
|
+
getValidationValue(value, target) {
|
|
17
|
+
return getCompiledDtoBindingPlan(target).toValidationValue(value);
|
|
27
18
|
}
|
|
28
19
|
async validate(value, target) {
|
|
29
20
|
try {
|
|
30
|
-
const
|
|
31
|
-
|
|
21
|
+
const plan = getCompiledDtoBindingPlan(target);
|
|
22
|
+
if (!plan.needsValidation) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
await this.validator.validate(this.getValidationValue(value, target), target);
|
|
32
26
|
} catch (error) {
|
|
33
27
|
if (error instanceof DtoValidationError) {
|
|
34
28
|
this.throwBadRequestForValidationError(error);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RequestScopeContainer } from '@fluojs/di';
|
|
1
2
|
import type { Binder, HandlerDescriptor, RequestContext } from '../types.js';
|
|
2
3
|
/**
|
|
3
4
|
* Invoke controller handler.
|
|
@@ -5,7 +6,8 @@ import type { Binder, HandlerDescriptor, RequestContext } from '../types.js';
|
|
|
5
6
|
* @param handler The handler.
|
|
6
7
|
* @param requestContext The request context.
|
|
7
8
|
* @param binder The binder.
|
|
9
|
+
* @param controllerContainer Container used for resolving the controller instance before handler invocation.
|
|
8
10
|
* @returns The invoke controller handler result.
|
|
9
11
|
*/
|
|
10
|
-
export declare function invokeControllerHandler(handler: HandlerDescriptor, requestContext: RequestContext, binder?: Binder): Promise<unknown>;
|
|
12
|
+
export declare function invokeControllerHandler(handler: HandlerDescriptor, requestContext: RequestContext, binder?: Binder, controllerContainer?: RequestScopeContainer): Promise<unknown>;
|
|
11
13
|
//# sourceMappingURL=dispatch-handler-policy.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatch-handler-policy.d.ts","sourceRoot":"","sources":["../../src/dispatch/dispatch-handler-policy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dispatch-handler-policy.d.ts","sourceRoot":"","sources":["../../src/dispatch/dispatch-handler-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAKxD,OAAO,KAAK,EAA2B,MAAM,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAKtG;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,iBAAiB,EAC1B,cAAc,EAAE,cAAc,EAC9B,MAAM,GAAE,MAAsB,EAC9B,mBAAmB,GAAE,qBAAgD,GACpE,OAAO,CAAC,OAAO,CAAC,CAwBlB"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { InvariantError } from '@fluojs/core';
|
|
2
2
|
import { DefaultBinder } from '../adapters/binding.js';
|
|
3
|
+
import { getCompiledDtoBindingPlan } from '../adapters/dto-binding-plan.js';
|
|
3
4
|
import { HttpDtoValidationAdapter } from '../adapters/dto-validation-adapter.js';
|
|
4
5
|
const defaultBinder = new DefaultBinder();
|
|
5
6
|
const defaultValidator = new HttpDtoValidationAdapter();
|
|
@@ -10,10 +11,11 @@ const defaultValidator = new HttpDtoValidationAdapter();
|
|
|
10
11
|
* @param handler The handler.
|
|
11
12
|
* @param requestContext The request context.
|
|
12
13
|
* @param binder The binder.
|
|
14
|
+
* @param controllerContainer Container used for resolving the controller instance before handler invocation.
|
|
13
15
|
* @returns The invoke controller handler result.
|
|
14
16
|
*/
|
|
15
|
-
export async function invokeControllerHandler(handler, requestContext, binder = defaultBinder) {
|
|
16
|
-
const controller = await
|
|
17
|
+
export async function invokeControllerHandler(handler, requestContext, binder = defaultBinder, controllerContainer = requestContext.container) {
|
|
18
|
+
const controller = await controllerContainer.resolve(handler.controllerToken);
|
|
17
19
|
const method = controller[handler.methodName];
|
|
18
20
|
if (typeof method !== 'function') {
|
|
19
21
|
throw new InvariantError(`Controller ${handler.controllerToken.name} does not expose handler method ${handler.methodName}.`);
|
|
@@ -22,9 +24,10 @@ export async function invokeControllerHandler(handler, requestContext, binder =
|
|
|
22
24
|
handler,
|
|
23
25
|
requestContext
|
|
24
26
|
};
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
const requestDto = handler.route.request;
|
|
28
|
+
const input = requestDto ? await binder.bind(requestDto, argumentResolverContext) : undefined;
|
|
29
|
+
if (requestDto && getCompiledDtoBindingPlan(requestDto).needsValidation) {
|
|
30
|
+
await defaultValidator.validate(input, requestDto);
|
|
28
31
|
}
|
|
29
32
|
return method.call(controller, input, requestContext);
|
|
30
33
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatch-response-policy.d.ts","sourceRoot":"","sources":["../../src/dispatch/dispatch-response-policy.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,yBAAyB,EAEzB,KAAK,0BAA0B,EAChC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"dispatch-response-policy.d.ts","sourceRoot":"","sources":["../../src/dispatch/dispatch-response-policy.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,yBAAyB,EAEzB,KAAK,0BAA0B,EAChC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAgErB;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,gBAAgB,EACzB,QAAQ,EAAE,iBAAiB,EAC3B,KAAK,EAAE,OAAO,EACd,kBAAkB,EAAE,0BAA0B,GAAG,SAAS,GACzD,OAAO,CAAC,IAAI,CAAC,CAyCf;AAED,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,CAAC;AACzD,YAAY,EAAE,0BAA0B,EAAE,CAAC"}
|
|
@@ -11,6 +11,34 @@ function resolveDefaultSuccessStatus(handler, value) {
|
|
|
11
11
|
return 200;
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
function canUseSimpleJsonFastPath(response, value) {
|
|
15
|
+
return isSimpleJsonResponseBody(value) && !isResponseBodyForbidden(response.statusCode) && hasJsonCompatibleContentType(response);
|
|
16
|
+
}
|
|
17
|
+
function hasSimpleJsonResponseWriter(response) {
|
|
18
|
+
return typeof response.sendSimpleJson === 'function';
|
|
19
|
+
}
|
|
20
|
+
function isSimpleJsonResponseBody(value) {
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return typeof value === 'object' && value !== null && Object.getPrototypeOf(value) === Object.prototype;
|
|
25
|
+
}
|
|
26
|
+
function isResponseBodyForbidden(status) {
|
|
27
|
+
return status === 204 || status === 205 || status === 304;
|
|
28
|
+
}
|
|
29
|
+
function hasJsonCompatibleContentType(response) {
|
|
30
|
+
const contentType = readHeader(response.headers, 'content-type');
|
|
31
|
+
return contentType === undefined || isJsonContentType(contentType);
|
|
32
|
+
}
|
|
33
|
+
function readHeader(headers, name) {
|
|
34
|
+
const lowerName = name.toLowerCase();
|
|
35
|
+
const entry = Object.entries(headers).find(([headerName]) => headerName.toLowerCase() === lowerName);
|
|
36
|
+
const value = entry?.[1];
|
|
37
|
+
return typeof value === 'string' ? value : undefined;
|
|
38
|
+
}
|
|
39
|
+
function isJsonContentType(contentType) {
|
|
40
|
+
return contentType.toLowerCase().includes('application/json') || contentType.toLowerCase().endsWith('+json');
|
|
41
|
+
}
|
|
14
42
|
|
|
15
43
|
/**
|
|
16
44
|
* Write success response.
|
|
@@ -50,6 +78,10 @@ export async function writeSuccessResponse(handler, request, response, value, co
|
|
|
50
78
|
} else if (response.statusSet !== true) {
|
|
51
79
|
response.setStatus(resolveDefaultSuccessStatus(handler, value));
|
|
52
80
|
}
|
|
81
|
+
if (!formatter && hasSimpleJsonResponseWriter(response) && canUseSimpleJsonFastPath(response, value)) {
|
|
82
|
+
await response.sendSimpleJson(value);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
53
85
|
const responseBody = formatter ? formatter.format(value) : value;
|
|
54
86
|
await response.send(responseBody);
|
|
55
87
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Container } from '@fluojs/di';
|
|
2
|
-
import type { Binder, ContentNegotiationOptions, Dispatcher, DispatcherLogger, FrameworkRequest, FrameworkResponse, HandlerMapping, InterceptorLike, MiddlewareLike, RequestObserverLike } from '../types.js';
|
|
2
|
+
import type { Binder, ContentNegotiationOptions, ConverterLike, Dispatcher, DispatcherLogger, FrameworkRequest, FrameworkResponse, HandlerMapping, InterceptorLike, MiddlewareLike, RequestObserverLike } from '../types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Type definition for a global HTTP error handler function.
|
|
5
5
|
*/
|
|
@@ -22,6 +22,11 @@ export interface CreateDispatcherOptions {
|
|
|
22
22
|
observers?: RequestObserverLike[];
|
|
23
23
|
/** Optional global error handler. */
|
|
24
24
|
onError?: ErrorHandler;
|
|
25
|
+
/** Request-scope optimization hints supplied by runtime bootstrap. */
|
|
26
|
+
requestScope?: {
|
|
27
|
+
/** Global DTO converters used by the default binder. */
|
|
28
|
+
converterDefinitions?: readonly ConverterLike[];
|
|
29
|
+
};
|
|
25
30
|
logger?: DispatcherLogger;
|
|
26
31
|
/** Root DI container for creating request scopes. */
|
|
27
32
|
rootContainer: Container;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/dispatch/dispatcher.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/dispatch/dispatcher.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAyB,MAAM,YAAY,CAAC;AAQnE,OAAO,KAAK,EACV,MAAM,EACN,yBAAyB,EACzB,aAAa,EACb,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EAIjB,cAAc,EACd,eAAe,EAEf,cAAc,EAId,mBAAmB,EACpB,MAAM,aAAa,CAAC;AAMrB;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,iBAAiB,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,OAAO,GAAG,IAAI,CAAC;AAEpK;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,iDAAiD;IACjD,aAAa,CAAC,EAAE,cAAc,EAAE,CAAC;IACjC,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,kBAAkB,CAAC,EAAE,yBAAyB,CAAC;IAC/C,sDAAsD;IACtD,cAAc,EAAE,cAAc,CAAC;IAC/B,2DAA2D;IAC3D,YAAY,CAAC,EAAE,eAAe,EAAE,CAAC;IACjC,0DAA0D;IAC1D,SAAS,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAClC,qCAAqC;IACrC,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,sEAAsE;IACtE,YAAY,CAAC,EAAE;QACb,wDAAwD;QACxD,oBAAoB,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;KACjD,CAAC;IACF,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,qDAAqD;IACrD,aAAa,EAAE,SAAS,CAAC;CAC1B;AAugBD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,UAAU,CA+D7E"}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { getCompiledDtoBindingPlan } from '../adapters/dto-binding-plan.js';
|
|
2
|
+
import { createRequestContext, runWithRequestContext } from '../context/request-context.js';
|
|
3
|
+
import { SseResponse } from '../context/sse.js';
|
|
4
4
|
import { RequestAbortedError } from '../errors.js';
|
|
5
5
|
import { runGuardChain } from '../guards.js';
|
|
6
6
|
import { runInterceptorChain } from '../interceptors.js';
|
|
7
|
-
import { runMiddlewareChain } from '../middleware/middleware.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { isMiddlewareRouteConfig, matchRoutePattern, runMiddlewareChain } from '../middleware/middleware.js';
|
|
8
|
+
import { invokeControllerHandler } from './dispatch-handler-policy.js';
|
|
9
|
+
import { resolveContentNegotiation, writeErrorResponse, writeSuccessResponse } from './dispatch-response-policy.js';
|
|
10
|
+
import { matchHandlerOrThrow, updateRequestParams } from './dispatch-routing-policy.js';
|
|
11
|
+
import { readFrameworkRequestNativeRouteHandoff } from './native-route-handoff.js';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Type definition for a global HTTP error handler function.
|
|
@@ -59,14 +61,120 @@ function readRequestId(request) {
|
|
|
59
61
|
const normalized = value?.trim();
|
|
60
62
|
return normalized ? normalized : undefined;
|
|
61
63
|
}
|
|
62
|
-
function createDispatchContext(request, response,
|
|
63
|
-
|
|
64
|
-
container
|
|
64
|
+
function createDispatchContext(request, response, container, promoteOnContainerAccess) {
|
|
65
|
+
const context = createRequestContext({
|
|
66
|
+
container,
|
|
65
67
|
metadata: {},
|
|
66
68
|
request,
|
|
67
69
|
requestId: readRequestId(request),
|
|
68
70
|
response
|
|
69
71
|
});
|
|
72
|
+
if (!promoteOnContainerAccess) {
|
|
73
|
+
return context;
|
|
74
|
+
}
|
|
75
|
+
let activeContainer = container;
|
|
76
|
+
Object.defineProperty(context, 'container', {
|
|
77
|
+
configurable: true,
|
|
78
|
+
enumerable: true,
|
|
79
|
+
get() {
|
|
80
|
+
activeContainer = promoteOnContainerAccess();
|
|
81
|
+
return activeContainer;
|
|
82
|
+
},
|
|
83
|
+
set(value) {
|
|
84
|
+
activeContainer = value;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
return context;
|
|
88
|
+
}
|
|
89
|
+
function createRootDispatchScope(rootContainer) {
|
|
90
|
+
return {
|
|
91
|
+
container: rootContainer,
|
|
92
|
+
requestScoped: false
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function createRequestDispatchScope(rootContainer) {
|
|
96
|
+
return {
|
|
97
|
+
container: rootContainer.createRequestScope(),
|
|
98
|
+
requestScoped: true
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function activeMiddlewareMayRequireRequestScope(definitions, request) {
|
|
102
|
+
return definitions.some(definition => {
|
|
103
|
+
if (!isMiddlewareRouteConfig(definition)) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
return definition.routes.length === 0 || definition.routes.some(route => matchRoutePattern(route, request.path));
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function compileMiddlewareScopePlan(definitions) {
|
|
110
|
+
const conditionalDefinitions = [];
|
|
111
|
+
for (const definition of definitions) {
|
|
112
|
+
if (!isMiddlewareRouteConfig(definition) || definition.routes.length === 0) {
|
|
113
|
+
return {
|
|
114
|
+
alwaysRequiresRequestScope: true,
|
|
115
|
+
conditionalDefinitions: []
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
conditionalDefinitions.push(definition);
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
alwaysRequiresRequestScope: false,
|
|
122
|
+
conditionalDefinitions
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function compiledMiddlewareMayRequireRequestScope(plan, request) {
|
|
126
|
+
return plan.alwaysRequiresRequestScope || activeMiddlewareMayRequireRequestScope(plan.conditionalDefinitions, request);
|
|
127
|
+
}
|
|
128
|
+
function requestDtoMayRequireRequestScope(handler, options) {
|
|
129
|
+
if (!handler.route.request) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
if ((options.requestScope?.converterDefinitions ?? []).length > 0) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
if (options.binder) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
const plan = getCompiledDtoBindingPlan(handler.route.request);
|
|
139
|
+
return plan.entries.some(entry => entry.converter !== undefined);
|
|
140
|
+
}
|
|
141
|
+
function handlerMethodMayUseRequestContext(handler) {
|
|
142
|
+
const method = handler.controllerToken.prototype[handler.methodName];
|
|
143
|
+
return typeof method === 'function' && method.length >= 2;
|
|
144
|
+
}
|
|
145
|
+
function hasRequestScopeInspector(container) {
|
|
146
|
+
return typeof container === 'object' && container !== null && 'hasRequestScopedDependency' in container && typeof container.hasRequestScopedDependency === 'function';
|
|
147
|
+
}
|
|
148
|
+
function compileHandlerExecutionPlan(handler, options) {
|
|
149
|
+
const routeGuards = handler.route.guards ?? [];
|
|
150
|
+
const requestScope = compileMiddlewareScopePlan(handler.metadata.moduleMiddleware);
|
|
151
|
+
const mergedInterceptors = mergeInterceptors(options.interceptors ?? [], handler.route.interceptors ?? []);
|
|
152
|
+
return {
|
|
153
|
+
mergedInterceptors,
|
|
154
|
+
requestScope,
|
|
155
|
+
requiresRequestScope: routeGuards.length > 0 || mergedInterceptors.length > 0 || requestScope.alwaysRequiresRequestScope || requestDtoMayRequireRequestScope(handler, options) || handlerMethodMayUseRequestContext(handler) || (hasRequestScopeInspector(options.rootContainer) ? options.rootContainer.hasRequestScopedDependency(handler.controllerToken) : true),
|
|
156
|
+
routeGuards
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function handlerMayRequireRequestScope(plan, request) {
|
|
160
|
+
return plan.requiresRequestScope || compiledMiddlewareMayRequireRequestScope(plan.requestScope, request);
|
|
161
|
+
}
|
|
162
|
+
function compileDispatchStartPlan(observers, appMiddleware) {
|
|
163
|
+
const requestScope = compileMiddlewareScopePlan(appMiddleware);
|
|
164
|
+
return {
|
|
165
|
+
requestScope,
|
|
166
|
+
requiresRequestScope: observers.length > 0 || requestScope.alwaysRequiresRequestScope
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function dispatchStartMayRequireRequestScope(plan, request) {
|
|
170
|
+
return plan.requiresRequestScope || compiledMiddlewareMayRequireRequestScope(plan.requestScope, request);
|
|
171
|
+
}
|
|
172
|
+
function ensureRequestScope(context) {
|
|
173
|
+
if (context.dispatchScope.requestScoped) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
context.dispatchScope = createRequestDispatchScope(context.options.rootContainer);
|
|
177
|
+
context.requestContext.container = context.dispatchScope.container;
|
|
70
178
|
}
|
|
71
179
|
function ensureRequestNotAborted(request) {
|
|
72
180
|
if (request.signal?.aborted) {
|
|
@@ -111,8 +219,8 @@ function mergeInterceptors(globalInterceptors, routeInterceptors) {
|
|
|
111
219
|
}
|
|
112
220
|
return [...globalInterceptors, ...routeInterceptors];
|
|
113
221
|
}
|
|
114
|
-
async function dispatchMatchedHandler(handler, requestContext, observers, contentNegotiation, binder,
|
|
115
|
-
const routeGuards =
|
|
222
|
+
async function dispatchMatchedHandler(handler, executionPlan, requestContext, controllerContainer, observers, contentNegotiation, binder, logger) {
|
|
223
|
+
const routeGuards = executionPlan.routeGuards;
|
|
116
224
|
if (routeGuards.length > 0) {
|
|
117
225
|
const guardContext = {
|
|
118
226
|
handler,
|
|
@@ -123,11 +231,10 @@ async function dispatchMatchedHandler(handler, requestContext, observers, conten
|
|
|
123
231
|
if (requestContext.response.committed) {
|
|
124
232
|
return;
|
|
125
233
|
}
|
|
126
|
-
const
|
|
127
|
-
const result = globalInterceptors.length === 0 && routeInterceptors.length === 0 ? await invokeControllerHandler(handler, requestContext, binder) : await runInterceptorChain(mergeInterceptors(globalInterceptors, routeInterceptors), {
|
|
234
|
+
const result = executionPlan.mergedInterceptors.length === 0 ? await invokeControllerHandler(handler, requestContext, binder, controllerContainer) : await runInterceptorChain(executionPlan.mergedInterceptors, {
|
|
128
235
|
handler,
|
|
129
236
|
requestContext
|
|
130
|
-
}, async () => invokeControllerHandler(handler, requestContext, binder));
|
|
237
|
+
}, async () => invokeControllerHandler(handler, requestContext, binder, controllerContainer));
|
|
131
238
|
ensureRequestNotAborted(requestContext.request);
|
|
132
239
|
if (!(result instanceof SseResponse) && !requestContext.response.committed) {
|
|
133
240
|
await writeSuccessResponse(handler, requestContext.request, requestContext.response, result, contentNegotiation);
|
|
@@ -136,6 +243,15 @@ async function dispatchMatchedHandler(handler, requestContext, observers, conten
|
|
|
136
243
|
await observer.onRequestSuccess?.(context, result);
|
|
137
244
|
}, logger, handler);
|
|
138
245
|
}
|
|
246
|
+
function resolveHandlerExecutionPlan(handler, executionPlans, options) {
|
|
247
|
+
const cached = executionPlans.get(handler);
|
|
248
|
+
if (cached) {
|
|
249
|
+
return cached;
|
|
250
|
+
}
|
|
251
|
+
const compiled = compileHandlerExecutionPlan(handler, options);
|
|
252
|
+
executionPlans.set(handler, compiled);
|
|
253
|
+
return compiled;
|
|
254
|
+
}
|
|
139
255
|
async function notifyRequestStart(context) {
|
|
140
256
|
await notifyObserversSafely(context.observers, context.requestContext, async (observer, observationContext) => {
|
|
141
257
|
await observer.onRequestStart?.(observationContext);
|
|
@@ -167,8 +283,12 @@ async function runDispatchPipeline(context) {
|
|
|
167
283
|
if (context.response.committed) {
|
|
168
284
|
return;
|
|
169
285
|
}
|
|
170
|
-
const match = matchHandlerOrThrow(context.options.handlerMapping, appMiddlewareContext.request);
|
|
286
|
+
const match = readFrameworkRequestNativeRouteHandoff(appMiddlewareContext.request) ?? matchHandlerOrThrow(context.options.handlerMapping, appMiddlewareContext.request);
|
|
171
287
|
context.matchedHandler = match.descriptor;
|
|
288
|
+
const executionPlan = resolveHandlerExecutionPlan(match.descriptor, context.handlerExecutionPlans, context.options);
|
|
289
|
+
if (handlerMayRequireRequestScope(executionPlan, appMiddlewareContext.request)) {
|
|
290
|
+
ensureRequestScope(context);
|
|
291
|
+
}
|
|
172
292
|
updateRequestParams(context.requestContext, match.params);
|
|
173
293
|
await notifyHandlerMatched(context, match.descriptor);
|
|
174
294
|
const moduleMiddlewareContext = {
|
|
@@ -177,7 +297,7 @@ async function runDispatchPipeline(context) {
|
|
|
177
297
|
response: context.response
|
|
178
298
|
};
|
|
179
299
|
await runMiddlewareChain(match.descriptor.metadata.moduleMiddleware ?? [], moduleMiddlewareContext, async () => {
|
|
180
|
-
await dispatchMatchedHandler(match.descriptor, context.requestContext, context.
|
|
300
|
+
await dispatchMatchedHandler(match.descriptor, executionPlan, context.requestContext, context.dispatchScope.container, context.observers, context.contentNegotiation, context.options.binder, context.options.logger);
|
|
181
301
|
});
|
|
182
302
|
});
|
|
183
303
|
}
|
|
@@ -202,16 +322,35 @@ async function handleDispatchError(context, error) {
|
|
|
202
322
|
export function createDispatcher(options) {
|
|
203
323
|
const contentNegotiation = resolveContentNegotiation(options.contentNegotiation);
|
|
204
324
|
const observers = options.observers ?? [];
|
|
325
|
+
const appMiddleware = options.appMiddleware ?? [];
|
|
326
|
+
const dispatchStartPlan = compileDispatchStartPlan(observers, appMiddleware);
|
|
327
|
+
const handlerExecutionPlans = new WeakMap();
|
|
328
|
+
for (const descriptor of options.handlerMapping.descriptors) {
|
|
329
|
+
handlerExecutionPlans.set(descriptor, compileHandlerExecutionPlan(descriptor, options));
|
|
330
|
+
}
|
|
205
331
|
const dispatcher = {
|
|
206
332
|
describeRoutes() {
|
|
207
333
|
return options.handlerMapping.descriptors.map(descriptor => cloneHandlerDescriptor(descriptor));
|
|
208
334
|
},
|
|
209
335
|
async dispatch(request, response) {
|
|
210
|
-
const
|
|
336
|
+
const dispatchRequest = createDispatchRequest(request);
|
|
337
|
+
const dispatchScope = dispatchStartMayRequireRequestScope(dispatchStartPlan, dispatchRequest) ? createRequestDispatchScope(options.rootContainer) : createRootDispatchScope(options.rootContainer);
|
|
338
|
+
let phaseContext;
|
|
339
|
+
let containerPromotionOpen = true;
|
|
340
|
+
const requestContext = createDispatchContext(dispatchRequest, response, dispatchScope.container, () => {
|
|
341
|
+
if (!containerPromotionOpen) {
|
|
342
|
+
return phaseContext.dispatchScope.container;
|
|
343
|
+
}
|
|
344
|
+
ensureRequestScope(phaseContext);
|
|
345
|
+
return phaseContext.dispatchScope.container;
|
|
346
|
+
});
|
|
347
|
+
phaseContext = {
|
|
211
348
|
contentNegotiation,
|
|
349
|
+
dispatchScope,
|
|
350
|
+
handlerExecutionPlans,
|
|
212
351
|
observers,
|
|
213
352
|
options,
|
|
214
|
-
requestContext
|
|
353
|
+
requestContext,
|
|
215
354
|
response
|
|
216
355
|
};
|
|
217
356
|
await runWithRequestContext(phaseContext.requestContext, async () => {
|
|
@@ -222,10 +361,13 @@ export function createDispatcher(options) {
|
|
|
222
361
|
await handleDispatchError(phaseContext, error);
|
|
223
362
|
} finally {
|
|
224
363
|
await notifyRequestFinish(phaseContext);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
364
|
+
containerPromotionOpen = false;
|
|
365
|
+
if (phaseContext.dispatchScope.requestScoped) {
|
|
366
|
+
try {
|
|
367
|
+
await phaseContext.dispatchScope.container.dispose();
|
|
368
|
+
} catch (error) {
|
|
369
|
+
logDispatchFailure(options.logger, 'Request-scoped container dispose threw an error.', error);
|
|
370
|
+
}
|
|
229
371
|
}
|
|
230
372
|
}
|
|
231
373
|
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { FrameworkRequest, HandlerMatch } from '../types.js';
|
|
2
|
+
/** Internal handoff payload that lets adapters skip duplicate route matching safely. */
|
|
3
|
+
export type NativeRouteHandoff = HandlerMatch;
|
|
4
|
+
/**
|
|
5
|
+
* Associates one adapter-selected route handoff with a raw platform request.
|
|
6
|
+
*
|
|
7
|
+
* Platform adapters call this before translating the native request into a
|
|
8
|
+
* `FrameworkRequest`, allowing the shared dispatcher to reuse the semantically
|
|
9
|
+
* safe native match without changing the public dispatcher surface.
|
|
10
|
+
*
|
|
11
|
+
* @param rawRequest Raw platform request object used as the lookup key.
|
|
12
|
+
* @param handoff Pre-matched descriptor and params selected by the adapter.
|
|
13
|
+
*/
|
|
14
|
+
export declare function bindRawRequestNativeRouteHandoff(rawRequest: object, handoff: NativeRouteHandoff): void;
|
|
15
|
+
/**
|
|
16
|
+
* Reads and clears a native route handoff previously bound to a raw request.
|
|
17
|
+
*
|
|
18
|
+
* Request factories consume this once while constructing `FrameworkRequest`
|
|
19
|
+
* instances so the handoff remains request-local and does not leak across
|
|
20
|
+
* platform object reuse.
|
|
21
|
+
*
|
|
22
|
+
* @param rawRequest Raw platform request object used as the lookup key.
|
|
23
|
+
* @returns The cloned handoff when one was registered for this request.
|
|
24
|
+
*/
|
|
25
|
+
export declare function consumeRawRequestNativeRouteHandoff(rawRequest: unknown): NativeRouteHandoff | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Stores a pre-matched native route handoff on one framework request.
|
|
28
|
+
*
|
|
29
|
+
* @param request Framework request that should carry the adapter-native match.
|
|
30
|
+
* @param handoff Pre-matched descriptor and params selected by the adapter.
|
|
31
|
+
* @returns The same request instance for fluent adapter construction.
|
|
32
|
+
*/
|
|
33
|
+
export declare function attachFrameworkRequestNativeRouteHandoff(request: FrameworkRequest, handoff: NativeRouteHandoff): FrameworkRequest;
|
|
34
|
+
/**
|
|
35
|
+
* Reads a pre-matched native route handoff from one framework request.
|
|
36
|
+
*
|
|
37
|
+
* @param request Framework request being dispatched.
|
|
38
|
+
* @returns The cloned handoff when the adapter supplied one.
|
|
39
|
+
*/
|
|
40
|
+
export declare function readFrameworkRequestNativeRouteHandoff(request: FrameworkRequest): NativeRouteHandoff | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Reports whether a request path depends on fluo's normalization semantics.
|
|
43
|
+
*
|
|
44
|
+
* Duplicate slashes and trailing slashes are intentionally normalized by the
|
|
45
|
+
* shared matcher. Adapters use this helper to keep those requests on the
|
|
46
|
+
* generic dispatcher path when native routing may not preserve identical path
|
|
47
|
+
* selection semantics.
|
|
48
|
+
*
|
|
49
|
+
* @param path Raw framework request path.
|
|
50
|
+
* @returns `true` when normalization would change the incoming path.
|
|
51
|
+
*/
|
|
52
|
+
export declare function isRoutePathNormalizationSensitive(path: string): boolean;
|
|
53
|
+
//# sourceMappingURL=native-route-handoff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-route-handoff.d.ts","sourceRoot":"","sources":["../../src/dispatch/native-route-handoff.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAsBlE,wFAAwF;AACxF,MAAM,MAAM,kBAAkB,GAAG,YAAY,CAAC;AAE9C;;;;;;;;;GASG;AACH,wBAAgB,gCAAgC,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAEtG;AAED;;;;;;;;;GASG;AACH,wBAAgB,mCAAmC,CAAC,UAAU,EAAE,OAAO,GAAG,kBAAkB,GAAG,SAAS,CAavG;AAED;;;;;;GAMG;AACH,wBAAgB,wCAAwC,CACtD,OAAO,EAAE,gBAAgB,EACzB,OAAO,EAAE,kBAAkB,GAC1B,gBAAgB,CAYlB;AAED;;;;;GAKG;AACH,wBAAgB,sCAAsC,CACpD,OAAO,EAAE,gBAAgB,GACxB,kBAAkB,GAAG,SAAS,CAWhC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iCAAiC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEvE"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { normalizeRoutePath } from '../route-path.js';
|
|
2
|
+
const FRAMEWORK_REQUEST_NATIVE_ROUTE_HANDOFF = Symbol('fluo.http.nativeRouteHandoff');
|
|
3
|
+
const RAW_REQUEST_NATIVE_ROUTE_HANDOFFS = new WeakMap();
|
|
4
|
+
function cloneNativeRouteHandoff(handoff) {
|
|
5
|
+
return {
|
|
6
|
+
descriptor: handoff.descriptor,
|
|
7
|
+
params: {
|
|
8
|
+
...handoff.params
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Internal handoff payload that lets adapters skip duplicate route matching safely. */
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Associates one adapter-selected route handoff with a raw platform request.
|
|
17
|
+
*
|
|
18
|
+
* Platform adapters call this before translating the native request into a
|
|
19
|
+
* `FrameworkRequest`, allowing the shared dispatcher to reuse the semantically
|
|
20
|
+
* safe native match without changing the public dispatcher surface.
|
|
21
|
+
*
|
|
22
|
+
* @param rawRequest Raw platform request object used as the lookup key.
|
|
23
|
+
* @param handoff Pre-matched descriptor and params selected by the adapter.
|
|
24
|
+
*/
|
|
25
|
+
export function bindRawRequestNativeRouteHandoff(rawRequest, handoff) {
|
|
26
|
+
RAW_REQUEST_NATIVE_ROUTE_HANDOFFS.set(rawRequest, cloneNativeRouteHandoff(handoff));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Reads and clears a native route handoff previously bound to a raw request.
|
|
31
|
+
*
|
|
32
|
+
* Request factories consume this once while constructing `FrameworkRequest`
|
|
33
|
+
* instances so the handoff remains request-local and does not leak across
|
|
34
|
+
* platform object reuse.
|
|
35
|
+
*
|
|
36
|
+
* @param rawRequest Raw platform request object used as the lookup key.
|
|
37
|
+
* @returns The cloned handoff when one was registered for this request.
|
|
38
|
+
*/
|
|
39
|
+
export function consumeRawRequestNativeRouteHandoff(rawRequest) {
|
|
40
|
+
if (typeof rawRequest !== 'object' || rawRequest === null) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const handoff = RAW_REQUEST_NATIVE_ROUTE_HANDOFFS.get(rawRequest);
|
|
44
|
+
if (!handoff) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
RAW_REQUEST_NATIVE_ROUTE_HANDOFFS.delete(rawRequest);
|
|
48
|
+
return cloneNativeRouteHandoff(handoff);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Stores a pre-matched native route handoff on one framework request.
|
|
53
|
+
*
|
|
54
|
+
* @param request Framework request that should carry the adapter-native match.
|
|
55
|
+
* @param handoff Pre-matched descriptor and params selected by the adapter.
|
|
56
|
+
* @returns The same request instance for fluent adapter construction.
|
|
57
|
+
*/
|
|
58
|
+
export function attachFrameworkRequestNativeRouteHandoff(request, handoff) {
|
|
59
|
+
Reflect.set(request, FRAMEWORK_REQUEST_NATIVE_ROUTE_HANDOFF, {
|
|
60
|
+
handoff: cloneNativeRouteHandoff(handoff),
|
|
61
|
+
method: request.method,
|
|
62
|
+
path: request.path
|
|
63
|
+
});
|
|
64
|
+
return request;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Reads a pre-matched native route handoff from one framework request.
|
|
69
|
+
*
|
|
70
|
+
* @param request Framework request being dispatched.
|
|
71
|
+
* @returns The cloned handoff when the adapter supplied one.
|
|
72
|
+
*/
|
|
73
|
+
export function readFrameworkRequestNativeRouteHandoff(request) {
|
|
74
|
+
const record = Reflect.get(request, FRAMEWORK_REQUEST_NATIVE_ROUTE_HANDOFF);
|
|
75
|
+
if (!record || record.method !== request.method || record.path !== request.path) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
return cloneNativeRouteHandoff(record.handoff);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Reports whether a request path depends on fluo's normalization semantics.
|
|
83
|
+
*
|
|
84
|
+
* Duplicate slashes and trailing slashes are intentionally normalized by the
|
|
85
|
+
* shared matcher. Adapters use this helper to keep those requests on the
|
|
86
|
+
* generic dispatcher path when native routing may not preserve identical path
|
|
87
|
+
* selection semantics.
|
|
88
|
+
*
|
|
89
|
+
* @param path Raw framework request path.
|
|
90
|
+
* @returns `true` when normalization would change the incoming path.
|
|
91
|
+
*/
|
|
92
|
+
export function isRoutePathNormalizationSensitive(path) {
|
|
93
|
+
return normalizeRoutePath(path) !== path;
|
|
94
|
+
}
|
package/dist/internal.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { DefaultBinder } from './adapters/binding.js';
|
|
2
2
|
export { resolveClientIdentity } from './client-identity.js';
|
|
3
|
+
export { attachFrameworkRequestNativeRouteHandoff, bindRawRequestNativeRouteHandoff, consumeRawRequestNativeRouteHandoff, isRoutePathNormalizationSensitive, readFrameworkRequestNativeRouteHandoff, type NativeRouteHandoff, } from './dispatch/native-route-handoff.js';
|
|
3
4
|
//# sourceMappingURL=internal.d.ts.map
|
package/dist/internal.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EACL,wCAAwC,EACxC,gCAAgC,EAChC,mCAAmC,EACnC,iCAAiC,EACjC,sCAAsC,EACtC,KAAK,kBAAkB,GACxB,MAAM,oCAAoC,CAAC"}
|
package/dist/internal.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { DefaultBinder } from './adapters/binding.js';
|
|
2
|
-
export { resolveClientIdentity } from './client-identity.js';
|
|
2
|
+
export { resolveClientIdentity } from './client-identity.js';
|
|
3
|
+
export { attachFrameworkRequestNativeRouteHandoff, bindRawRequestNativeRouteHandoff, consumeRawRequestNativeRouteHandoff, isRoutePathNormalizationSensitive, readFrameworkRequestNativeRouteHandoff } from './dispatch/native-route-handoff.js';
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"controller",
|
|
11
11
|
"rest"
|
|
12
12
|
],
|
|
13
|
-
"version": "1.0.0-beta.
|
|
13
|
+
"version": "1.0.0-beta.5",
|
|
14
14
|
"private": false,
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"repository": {
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@fluojs/core": "^1.0.0-beta.2",
|
|
45
|
-
"@fluojs/
|
|
46
|
-
"@fluojs/
|
|
45
|
+
"@fluojs/validation": "^1.0.0-beta.1",
|
|
46
|
+
"@fluojs/di": "^1.0.0-beta.5"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"vitest": "^3.2.4"
|