@loutrejs/loutre 0.6.0 → 0.8.0
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/adapters/bun.d.ts +2 -4
- package/dist/adapters/bun.js +36 -28
- package/dist/adapters/cloudflare-workers.d.ts +2 -4
- package/dist/adapters/cloudflare-workers.js +6 -11
- package/dist/adapters/server-transports.d.ts +4 -0
- package/dist/adapters/server-transports.js +18 -0
- package/dist/adapters/websocket-native.d.ts +24 -0
- package/dist/adapters/websocket-native.js +150 -0
- package/dist/application/kernel.d.ts +2 -1
- package/dist/core/extension.d.ts +26 -3
- package/dist/core/extension.js +19 -0
- package/dist/core/index.d.ts +2 -1
- package/dist/core/instrumentation.d.ts +36 -0
- package/dist/core/instrumentation.js +1 -0
- package/dist/core/model/modules.js +13 -17
- package/dist/core/model.js +14 -4
- package/dist/core/module.d.ts +3 -3
- package/dist/devtools/capsule.d.ts +51 -0
- package/dist/devtools/capsule.js +247 -0
- package/dist/devtools/index.d.ts +11 -0
- package/dist/devtools/index.js +6 -0
- package/dist/devtools/inspector.d.ts +57 -0
- package/dist/devtools/inspector.js +1 -0
- package/dist/devtools/module.d.ts +18 -0
- package/dist/devtools/module.js +26 -0
- package/dist/devtools/observer.d.ts +77 -0
- package/dist/devtools/observer.js +10 -0
- package/dist/devtools/protocol.d.ts +1 -0
- package/dist/devtools/protocol.js +1 -0
- package/dist/devtools/provider-instrumentation.d.ts +14 -0
- package/dist/devtools/provider-instrumentation.js +117 -0
- package/dist/devtools/runtime.d.ts +20 -0
- package/dist/devtools/runtime.js +519 -0
- package/dist/devtools/values.d.ts +35 -0
- package/dist/devtools/values.js +172 -0
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/graph/model.js +37 -40
- package/dist/http/auth.d.ts +2 -2
- package/dist/http/auth.js +9 -0
- package/dist/http/client.js +5 -5
- package/dist/http/extension.d.ts +37 -10
- package/dist/http/extension.js +249 -57
- package/dist/http/index.d.ts +3 -2
- package/dist/http/index.js +1 -1
- package/dist/http/openapi.d.ts +3 -4
- package/dist/http/openapi.js +45 -26
- package/dist/http/request-head.d.ts +6 -0
- package/dist/http/request-head.js +1 -0
- package/dist/http/route-validation.d.ts +9 -0
- package/dist/http/route-validation.js +18 -0
- package/dist/http/runtime/response.js +10 -10
- package/dist/message-port/extension.js +20 -4
- package/dist/runtime/di.d.ts +4 -0
- package/dist/runtime/di.js +42 -31
- package/dist/runtime/engine.d.ts +1 -1
- package/dist/runtime/engine.js +1 -2
- package/dist/runtime/kernel.d.ts +4 -2
- package/dist/runtime/kernel.js +96 -3
- package/dist/runtime/node-capabilities.d.ts +1 -1
- package/dist/runtime/node-capabilities.js +2 -1
- package/dist/tasks/extension.js +133 -118
- package/dist/websocket/driver.d.ts +30 -0
- package/dist/websocket/driver.js +96 -0
- package/dist/websocket/extension.d.ts +33 -12
- package/dist/websocket/extension.js +72 -47
- package/dist/websocket/index.d.ts +3 -1
- package/dist/websocket/index.js +1 -0
- package/package.json +7 -6
- package/dist/adapters/deno.d.ts +0 -35
- package/dist/adapters/deno.js +0 -231
package/dist/http/extension.js
CHANGED
|
@@ -1,18 +1,35 @@
|
|
|
1
1
|
import { collectInjectedDependencies, getSourceLocation, inheritSourceLocation, composeLayers, defineExecution, defineExecutionExtension, defineLayer, runInInjectionContext, runtimeCapability, SchemaValidationError, validateSchema, } from '../core/index.js';
|
|
2
2
|
import { createCorsActualResponseHeaders, createCorsPreflightResponseHeaders, } from './cors-internal.js';
|
|
3
3
|
import { assertValidHttpMethod, compareHttpPathSpecificity, createHttpDispatchKey, HttpPathDecodeError, matchHttpPath, parseHttpPath, } from './path.js';
|
|
4
|
+
import { findHttpRouteConflicts } from './route-validation.js';
|
|
4
5
|
import { IngressGate } from '../runtime/ingress-gate.js';
|
|
5
6
|
import { AsyncIteratorCleanupDeadlineError, AsyncIteratorLifecycle, } from '../runtime/async-iterator-lifecycle.js';
|
|
6
7
|
import { decodeBody, decodeQuery, HttpInputDecodeError, HttpInputValidationError, HttpUnsupportedMediaTypeError, validatedContentType, validateRequestHeaders, } from './runtime/request.js';
|
|
7
8
|
import { applyFrameworkHeadersToResponse, applyFrameworkResponseHeaders, applyResponseHeaders, isResponseHeadersWithDefaults, isStandardSchema, mergeResponseHeaders, responseHeadersDefaults, responseHeadersSchema, validateResponseHeaders, } from './runtime/response.js';
|
|
9
|
+
import { match as patternMatch } from 'ts-pattern';
|
|
8
10
|
export const HTTP_SERVER = runtimeCapability('http.server');
|
|
11
|
+
const httpContractBrand = Symbol.for('loutre.http-contract');
|
|
12
|
+
export function isHttpContract(value) {
|
|
13
|
+
if (typeof value !== 'object' || value === null)
|
|
14
|
+
return false;
|
|
15
|
+
const candidate = value;
|
|
16
|
+
return (candidate.kind === 'http-contract' &&
|
|
17
|
+
candidate[httpContractBrand] === true &&
|
|
18
|
+
typeof candidate.routes === 'object' &&
|
|
19
|
+
candidate.routes !== null);
|
|
20
|
+
}
|
|
21
|
+
export function defineHttpRaw(definition) {
|
|
22
|
+
return defineExecution(httpExecutionExtension, { ...definition, raw: true });
|
|
23
|
+
}
|
|
9
24
|
export const httpExecutionExtension = defineExecutionExtension({
|
|
10
25
|
kind: 'execution-extension',
|
|
11
26
|
abiVersion: '1',
|
|
12
27
|
name: 'loutre:http',
|
|
13
28
|
compile(definition, context) {
|
|
14
|
-
const
|
|
15
|
-
const routes =
|
|
29
|
+
const raw = 'route' in definition;
|
|
30
|
+
const routes = raw
|
|
31
|
+
? [compileRawRoute(definition.route)]
|
|
32
|
+
: Object.entries(definition.contract.routes).map(([name, route]) => compileHttpRoute(name, route, getSourceLocation(definition.contract)));
|
|
16
33
|
const id = definition.name || `${context.moduleId}.http.${context.definitionIndex}`;
|
|
17
34
|
const dependencies = new Set(collectInjectedDependencies({
|
|
18
35
|
kind: 'execution',
|
|
@@ -43,25 +60,22 @@ export const httpExecutionExtension = defineExecutionExtension({
|
|
|
43
60
|
],
|
|
44
61
|
compiled: Object.freeze({
|
|
45
62
|
routes: Object.freeze(routes),
|
|
46
|
-
factory:
|
|
63
|
+
factory: (raw
|
|
64
|
+
? () => ({ raw: definition.factory() })
|
|
65
|
+
: definition.factory),
|
|
47
66
|
}),
|
|
48
67
|
};
|
|
49
68
|
},
|
|
50
69
|
validate({ executions }) {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
},
|
|
61
|
-
];
|
|
62
|
-
}
|
|
63
|
-
dispatches.set(route.dispatch, execution.id);
|
|
64
|
-
return [];
|
|
70
|
+
const routes = executions.flatMap((execution) => execution.compiled.routes.map((route) => ({
|
|
71
|
+
method: route.method,
|
|
72
|
+
path: route.path,
|
|
73
|
+
owner: execution.id,
|
|
74
|
+
})));
|
|
75
|
+
return findHttpRouteConflicts(routes).map(({ existing, route }) => ({
|
|
76
|
+
code: 'LUTRE_HTTP_DUPLICATE_ROUTE',
|
|
77
|
+
message: `${route.method} ${route.path} conflicts with ${existing.owner}.`,
|
|
78
|
+
path: route.owner,
|
|
65
79
|
}));
|
|
66
80
|
},
|
|
67
81
|
createRuntime(context) {
|
|
@@ -81,10 +95,16 @@ export const httpExecutionExtension = defineExecutionExtension({
|
|
|
81
95
|
? {}
|
|
82
96
|
: { source: getSourceLocation(middleware) }),
|
|
83
97
|
})),
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
98
|
+
...(route.raw
|
|
99
|
+
? {
|
|
100
|
+
raw: true,
|
|
101
|
+
...(route.protocol === undefined
|
|
102
|
+
? {}
|
|
103
|
+
: { protocol: route.protocol }),
|
|
104
|
+
}
|
|
105
|
+
: {
|
|
106
|
+
responses: Object.fromEntries(Object.entries(route.definition.responses).map(([name, response]) => [name, { status: response.status }])),
|
|
107
|
+
}),
|
|
88
108
|
})),
|
|
89
109
|
}),
|
|
90
110
|
host: {
|
|
@@ -101,12 +121,14 @@ export function httpError(definition, map) {
|
|
|
101
121
|
}
|
|
102
122
|
export function defineHttpContract(routes) {
|
|
103
123
|
const resolvedRoutes = resolveHttpContractRoutes(routes);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
124
|
+
const canonicalRoutes = Object.fromEntries(Object.entries(resolvedRoutes).map(([name, route]) => [
|
|
125
|
+
name,
|
|
126
|
+
compileHttpRoute(name, route).definition,
|
|
127
|
+
]));
|
|
107
128
|
return Object.freeze({
|
|
108
129
|
kind: 'http-contract',
|
|
109
|
-
|
|
130
|
+
[httpContractBrand]: true,
|
|
131
|
+
routes: Object.freeze(canonicalRoutes),
|
|
110
132
|
});
|
|
111
133
|
}
|
|
112
134
|
function resolveHttpContractRoutes(tree) {
|
|
@@ -186,6 +208,7 @@ export const validate = Object.freeze({
|
|
|
186
208
|
body: bodyValidation,
|
|
187
209
|
});
|
|
188
210
|
export const executionHttp = Object.freeze({
|
|
211
|
+
raw: defineHttpRaw,
|
|
189
212
|
contract: defineHttpContract,
|
|
190
213
|
implementation: defineHttpImplementation,
|
|
191
214
|
middleware: defineHttpMiddleware,
|
|
@@ -203,7 +226,29 @@ export function withHttpFrameworkHeaders(result, headers) {
|
|
|
203
226
|
[httpFrameworkHeaders]: { ...current, ...headers },
|
|
204
227
|
};
|
|
205
228
|
}
|
|
229
|
+
function compileRawRoute(route) {
|
|
230
|
+
if (route.method !== '*')
|
|
231
|
+
assertValidHttpMethod(route.method);
|
|
232
|
+
const segments = parseHttpPath(route.path);
|
|
233
|
+
return Object.freeze({
|
|
234
|
+
name: 'raw',
|
|
235
|
+
method: route.method.toUpperCase(),
|
|
236
|
+
path: route.path,
|
|
237
|
+
segments,
|
|
238
|
+
dispatch: createHttpDispatchKey(route.method, segments),
|
|
239
|
+
definition: Object.freeze({
|
|
240
|
+
method: route.method,
|
|
241
|
+
path: route.path,
|
|
242
|
+
responses: Object.freeze({}),
|
|
243
|
+
}),
|
|
244
|
+
middlewares: Object.freeze([...(route.middlewares ?? [])]),
|
|
245
|
+
raw: true,
|
|
246
|
+
...(route.protocol === undefined ? {} : { protocol: route.protocol }),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
206
249
|
function compileHttpRoute(name, route, routeSourceFallback) {
|
|
250
|
+
if (route.method === '*')
|
|
251
|
+
throw new TypeError('method: *はhttp.raw()でのみ利用できます。');
|
|
207
252
|
assertValidHttpMethod(route.method);
|
|
208
253
|
const segments = parseHttpPath(route.path);
|
|
209
254
|
assertValidHttpRouteDefinition(route, segments);
|
|
@@ -281,15 +326,15 @@ function compileHttpMiddlewares(route) {
|
|
|
281
326
|
return Object.freeze(compiled);
|
|
282
327
|
}
|
|
283
328
|
function compileHttpValidationMiddleware(middleware, route) {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
return createBodyValidationMiddleware(schema);
|
|
329
|
+
return patternMatch(middleware)
|
|
330
|
+
.with({ part: 'body' }, () => {
|
|
331
|
+
const schema = route.request?.body;
|
|
332
|
+
if (!schema) {
|
|
333
|
+
throw new TypeError(`HTTP ${route.method} ${route.path} uses validate.body but declares no request body.`);
|
|
291
334
|
}
|
|
292
|
-
|
|
335
|
+
return createBodyValidationMiddleware(schema);
|
|
336
|
+
})
|
|
337
|
+
.exhaustive();
|
|
293
338
|
}
|
|
294
339
|
function createBodyValidationMiddleware(schema) {
|
|
295
340
|
return defineLayer({
|
|
@@ -409,7 +454,12 @@ function createHttpExtensionRuntime(executions, applicationRuntime) {
|
|
|
409
454
|
}
|
|
410
455
|
let lease;
|
|
411
456
|
try {
|
|
412
|
-
lease = applicationRuntime.beginExecution(
|
|
457
|
+
lease = applicationRuntime.beginExecution({
|
|
458
|
+
executionId: match.executionId,
|
|
459
|
+
executionKind: 'http.request',
|
|
460
|
+
graphNodeId: match.executionId,
|
|
461
|
+
name: `OPTIONS ${match.route.path}`,
|
|
462
|
+
});
|
|
413
463
|
}
|
|
414
464
|
catch (error) {
|
|
415
465
|
completePendingIngress();
|
|
@@ -418,6 +468,7 @@ function createHttpExtensionRuntime(executions, applicationRuntime) {
|
|
|
418
468
|
try {
|
|
419
469
|
const headers = await createCorsPreflightResponseHeaders(match.route.middlewares, request, match.route.method);
|
|
420
470
|
if (headers) {
|
|
471
|
+
lease.annotate?.({ 'http.status_code': 204 });
|
|
421
472
|
return new Response(null, { status: 204, headers });
|
|
422
473
|
}
|
|
423
474
|
}
|
|
@@ -445,7 +496,30 @@ function createHttpExtensionRuntime(executions, applicationRuntime) {
|
|
|
445
496
|
return Response.json({ error: 'Internal Server Error' }, { status: 500 });
|
|
446
497
|
}
|
|
447
498
|
if (!match) {
|
|
448
|
-
|
|
499
|
+
const completePendingIngress = ingress.enter();
|
|
500
|
+
if (!completePendingIngress) {
|
|
501
|
+
return Response.json({ error: 'Service Unavailable' }, { status: 503 });
|
|
502
|
+
}
|
|
503
|
+
let lease;
|
|
504
|
+
try {
|
|
505
|
+
lease = applicationRuntime.beginExecution({
|
|
506
|
+
executionId: 'http.not-found',
|
|
507
|
+
executionKind: 'http.request',
|
|
508
|
+
name: `${method} ${url.pathname}`,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
completePendingIngress();
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
lease.annotate?.({ 'http.status_code': 404 });
|
|
517
|
+
return Response.json({ error: 'Not Found' }, { status: 404 });
|
|
518
|
+
}
|
|
519
|
+
finally {
|
|
520
|
+
lease.complete();
|
|
521
|
+
completePendingIngress();
|
|
522
|
+
}
|
|
449
523
|
}
|
|
450
524
|
const completePendingIngress = ingress.enter();
|
|
451
525
|
if (!completePendingIngress) {
|
|
@@ -453,7 +527,12 @@ function createHttpExtensionRuntime(executions, applicationRuntime) {
|
|
|
453
527
|
}
|
|
454
528
|
let lease;
|
|
455
529
|
try {
|
|
456
|
-
lease = applicationRuntime.beginExecution(
|
|
530
|
+
lease = applicationRuntime.beginExecution({
|
|
531
|
+
executionId: match.executionId,
|
|
532
|
+
executionKind: 'http.request',
|
|
533
|
+
graphNodeId: match.executionId,
|
|
534
|
+
name: `${match.route.method} ${match.route.path}`,
|
|
535
|
+
});
|
|
457
536
|
}
|
|
458
537
|
catch (error) {
|
|
459
538
|
completePendingIngress();
|
|
@@ -462,12 +541,17 @@ function createHttpExtensionRuntime(executions, applicationRuntime) {
|
|
|
462
541
|
const abortRequest = () => lease.abort(request.signal.reason);
|
|
463
542
|
let executionOwnedByStream = false;
|
|
464
543
|
let executionFinished = false;
|
|
544
|
+
let executionResult;
|
|
545
|
+
let hasExecutionResult = false;
|
|
465
546
|
const finishExecution = () => {
|
|
466
547
|
if (executionFinished)
|
|
467
548
|
return;
|
|
468
549
|
executionFinished = true;
|
|
469
550
|
request.signal.removeEventListener('abort', abortRequest);
|
|
470
|
-
|
|
551
|
+
if (hasExecutionResult)
|
|
552
|
+
lease.complete(executionResult);
|
|
553
|
+
else
|
|
554
|
+
lease.complete();
|
|
471
555
|
};
|
|
472
556
|
const adoptResponseStream = (stream) => {
|
|
473
557
|
executionOwnedByStream = true;
|
|
@@ -486,9 +570,13 @@ function createHttpExtensionRuntime(executions, applicationRuntime) {
|
|
|
486
570
|
request.signal.addEventListener('abort', abortRequest, { once: true });
|
|
487
571
|
if (request.signal.aborted)
|
|
488
572
|
abortRequest();
|
|
573
|
+
const annotateResponse = (response) => {
|
|
574
|
+
lease.annotate?.({ 'http.status_code': response.status });
|
|
575
|
+
return response;
|
|
576
|
+
};
|
|
489
577
|
try {
|
|
490
578
|
const corsHeaders = await createCorsActualResponseHeaders(match.route.middlewares, request);
|
|
491
|
-
const complete = (response) => applyFrameworkHeadersToResponse(response, corsHeaders);
|
|
579
|
+
const complete = (response) => annotateResponse(applyFrameworkHeadersToResponse(response, corsHeaders));
|
|
492
580
|
let context;
|
|
493
581
|
try {
|
|
494
582
|
context = await createHttpContext(request, url, match.params, match.route, lease.signal);
|
|
@@ -509,17 +597,83 @@ function createHttpExtensionRuntime(executions, applicationRuntime) {
|
|
|
509
597
|
if (!handler) {
|
|
510
598
|
throw new Error(`LUTRE_HTTP_HANDLER_MISSING: ${match.executionId}.${match.route.name}`);
|
|
511
599
|
}
|
|
512
|
-
const
|
|
600
|
+
const dispatch = () => composeLayers({
|
|
513
601
|
context,
|
|
514
602
|
layers: match.route.middlewares,
|
|
515
603
|
resolve: (token, source) => applicationRuntime.resolve(token, source),
|
|
516
|
-
terminal: async (middlewareContext) =>
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
604
|
+
terminal: async (middlewareContext) => {
|
|
605
|
+
const handlerName = `${match.executionId}.${match.route.name}`;
|
|
606
|
+
const handlerOperation = applicationRuntime.beginOperation?.({
|
|
607
|
+
kind: 'http.handler',
|
|
608
|
+
name: handlerName,
|
|
609
|
+
graphNodeId: match.executionId,
|
|
610
|
+
}, match.route.raw
|
|
611
|
+
? undefined
|
|
612
|
+
: {
|
|
613
|
+
input: middlewareContext.input,
|
|
614
|
+
invoke: (input) => replayHttpHandler(applicationRuntime, handler, match, input, middlewareContext.state),
|
|
615
|
+
});
|
|
616
|
+
const invoke = () => handler({
|
|
617
|
+
...(match.route.raw
|
|
618
|
+
? {
|
|
619
|
+
request,
|
|
620
|
+
execution: {
|
|
621
|
+
signal: lease.signal,
|
|
622
|
+
annotate: (attributes) => lease.annotate?.(attributes),
|
|
623
|
+
beginOperation: (metadata) => {
|
|
624
|
+
const begin = () => applicationRuntime.beginOperation?.(metadata) ?? {
|
|
625
|
+
complete() { },
|
|
626
|
+
};
|
|
627
|
+
return lease.run ? lease.run(begin) : begin();
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
}
|
|
631
|
+
: {}),
|
|
632
|
+
input: middlewareContext.input,
|
|
633
|
+
response: middlewareContext.response,
|
|
634
|
+
signal: match.route.raw
|
|
635
|
+
? request.signal
|
|
636
|
+
: middlewareContext.signal,
|
|
637
|
+
state: middlewareContext.state,
|
|
638
|
+
});
|
|
639
|
+
let result;
|
|
640
|
+
let hasResult = false;
|
|
641
|
+
try {
|
|
642
|
+
result = await (handlerOperation?.run
|
|
643
|
+
? handlerOperation.run(invoke)
|
|
644
|
+
: invoke());
|
|
645
|
+
hasResult = true;
|
|
646
|
+
return result;
|
|
647
|
+
}
|
|
648
|
+
catch (error) {
|
|
649
|
+
handlerOperation?.fail?.(error);
|
|
650
|
+
throw error;
|
|
651
|
+
}
|
|
652
|
+
finally {
|
|
653
|
+
if (hasResult)
|
|
654
|
+
handlerOperation?.complete(result);
|
|
655
|
+
else
|
|
656
|
+
handlerOperation?.complete();
|
|
657
|
+
}
|
|
658
|
+
},
|
|
522
659
|
});
|
|
660
|
+
const result = await (lease.run ? lease.run(dispatch) : dispatch());
|
|
661
|
+
executionResult = result;
|
|
662
|
+
hasExecutionResult = true;
|
|
663
|
+
if (result instanceof Response) {
|
|
664
|
+
if (result.body) {
|
|
665
|
+
const stream = createLeasedReadableStream(result.body, lease.signal, finishExecution);
|
|
666
|
+
adoptResponseStream(stream);
|
|
667
|
+
return complete(new Response(stream.stream, {
|
|
668
|
+
status: result.status,
|
|
669
|
+
statusText: result.statusText,
|
|
670
|
+
headers: result.headers,
|
|
671
|
+
}));
|
|
672
|
+
}
|
|
673
|
+
return complete(result);
|
|
674
|
+
}
|
|
675
|
+
if (match.route.raw)
|
|
676
|
+
throw new TypeError('Raw handlerとmiddlewareはResponseを返す必要があります。');
|
|
523
677
|
const finalized = await finalizeHttpResult(match.route.definition, result, lease.signal, finishExecution);
|
|
524
678
|
if (finalized.streamControl)
|
|
525
679
|
adoptResponseStream(finalized.streamControl);
|
|
@@ -527,11 +681,12 @@ function createHttpExtensionRuntime(executions, applicationRuntime) {
|
|
|
527
681
|
}
|
|
528
682
|
catch (error) {
|
|
529
683
|
if (error instanceof HttpInputDecodeError) {
|
|
530
|
-
return applyFrameworkHeadersToResponse(Response.json({ error: 'Invalid request' }, { status: 400 }), await safeCorsHeaders(match.route.middlewares, request));
|
|
684
|
+
return annotateResponse(applyFrameworkHeadersToResponse(Response.json({ error: 'Invalid request' }, { status: 400 }), await safeCorsHeaders(match.route.middlewares, request)));
|
|
531
685
|
}
|
|
532
686
|
if (error instanceof HttpInputValidationError) {
|
|
533
|
-
return applyFrameworkHeadersToResponse(Response.json({ error: 'Validation failed' }, { status: 400 }), await safeCorsHeaders(match.route.middlewares, request));
|
|
687
|
+
return annotateResponse(applyFrameworkHeadersToResponse(Response.json({ error: 'Validation failed' }, { status: 400 }), await safeCorsHeaders(match.route.middlewares, request)));
|
|
534
688
|
}
|
|
689
|
+
lease.fail?.(error);
|
|
535
690
|
try {
|
|
536
691
|
const mapped = await mapDeclaredError(match.route.definition, error);
|
|
537
692
|
if (mapped) {
|
|
@@ -539,13 +694,13 @@ function createHttpExtensionRuntime(executions, applicationRuntime) {
|
|
|
539
694
|
if (finalized.streamControl) {
|
|
540
695
|
adoptResponseStream(finalized.streamControl);
|
|
541
696
|
}
|
|
542
|
-
return applyFrameworkHeadersToResponse(finalized.response, await safeCorsHeaders(match.route.middlewares, request));
|
|
697
|
+
return annotateResponse(applyFrameworkHeadersToResponse(finalized.response, await safeCorsHeaders(match.route.middlewares, request)));
|
|
543
698
|
}
|
|
544
699
|
}
|
|
545
700
|
catch {
|
|
546
701
|
// 内部のmapping/finalization errorをclientへ公開しない。
|
|
547
702
|
}
|
|
548
|
-
return applyFrameworkHeadersToResponse(Response.json({ error: 'Internal Server Error' }, { status: 500 }), await safeCorsHeaders(match.route.middlewares, request));
|
|
703
|
+
return annotateResponse(applyFrameworkHeadersToResponse(Response.json({ error: 'Internal Server Error' }, { status: 500 }), await safeCorsHeaders(match.route.middlewares, request)));
|
|
549
704
|
}
|
|
550
705
|
finally {
|
|
551
706
|
if (!executionOwnedByStream)
|
|
@@ -557,7 +712,7 @@ function createHttpExtensionRuntime(executions, applicationRuntime) {
|
|
|
557
712
|
}
|
|
558
713
|
function findRuntimeHttpRoute(routes, method, pathname) {
|
|
559
714
|
for (const candidate of routes) {
|
|
560
|
-
if (candidate.route.method !== method)
|
|
715
|
+
if (candidate.route.method !== method && candidate.route.method !== '*')
|
|
561
716
|
continue;
|
|
562
717
|
const params = matchHttpPath(candidate.route.segments, pathname);
|
|
563
718
|
if (!params)
|
|
@@ -595,7 +750,16 @@ async function createHttpContext(request, url, rawParams, route, signal) {
|
|
|
595
750
|
? await validateRequestHeaders(definition.headers, definition.body !== undefined, request.headers)
|
|
596
751
|
: Object.fromEntries(request.headers.entries());
|
|
597
752
|
const body = definition?.body ? request.body : undefined;
|
|
598
|
-
const response =
|
|
753
|
+
const response = createHttpResponseHelpers(route.definition.responses);
|
|
754
|
+
return {
|
|
755
|
+
request,
|
|
756
|
+
input: { params, query, headers, body },
|
|
757
|
+
response,
|
|
758
|
+
signal,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
function createHttpResponseHelpers(responses) {
|
|
762
|
+
return Object.fromEntries(Object.keys(responses).map((name) => [
|
|
599
763
|
name,
|
|
600
764
|
(value = {}) => ({
|
|
601
765
|
kind: 'http-result',
|
|
@@ -604,12 +768,40 @@ async function createHttpContext(request, url, rawParams, route, signal) {
|
|
|
604
768
|
...(value.headers === undefined ? {} : { headers: value.headers }),
|
|
605
769
|
}),
|
|
606
770
|
]));
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
771
|
+
}
|
|
772
|
+
async function replayHttpHandler(applicationRuntime, handler, match, input, state) {
|
|
773
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
774
|
+
throw new TypeError('LUTRE_DEVTOOLS_HTTP_HANDLER_INPUT_REQUIRED');
|
|
775
|
+
}
|
|
776
|
+
const lease = applicationRuntime.beginExecution({
|
|
777
|
+
executionId: match.executionId,
|
|
778
|
+
executionKind: 'http.handler',
|
|
779
|
+
graphNodeId: match.executionId,
|
|
780
|
+
name: `${match.executionId}.${match.route.name}`,
|
|
781
|
+
});
|
|
782
|
+
const invoke = () => handler({
|
|
783
|
+
input,
|
|
784
|
+
response: createHttpResponseHelpers(match.route.definition.responses),
|
|
785
|
+
signal: lease.signal,
|
|
786
|
+
state,
|
|
787
|
+
});
|
|
788
|
+
let result;
|
|
789
|
+
let hasResult = false;
|
|
790
|
+
try {
|
|
791
|
+
result = await (lease.run ? lease.run(invoke) : invoke());
|
|
792
|
+
hasResult = true;
|
|
793
|
+
return result;
|
|
794
|
+
}
|
|
795
|
+
catch (error) {
|
|
796
|
+
lease.fail?.(error);
|
|
797
|
+
throw error;
|
|
798
|
+
}
|
|
799
|
+
finally {
|
|
800
|
+
if (hasResult)
|
|
801
|
+
lease.complete(result);
|
|
802
|
+
else
|
|
803
|
+
lease.complete();
|
|
804
|
+
}
|
|
613
805
|
}
|
|
614
806
|
async function finalizeHttpResult(route, result, signal, completeExecution) {
|
|
615
807
|
const response = route.responses[result.response];
|
package/dist/http/index.d.ts
CHANGED
|
@@ -4,7 +4,8 @@ export { cors } from './cors.js';
|
|
|
4
4
|
export type { CorsLayerDescriptor, CorsOptions, CorsOrigin } from './cors.js';
|
|
5
5
|
export { basicAuth, bearerAuth } from './auth.js';
|
|
6
6
|
export type { BasicAuthContext, BasicAuthCredentials, BasicAuthDefinition, BasicAuthUnauthorized, BasicAuthRuntime, BearerAuthContext, BearerAuthDefinition, BearerAuthRuntime, BearerAuthUnauthorized, HttpAuthenticationFailure, } from './auth.js';
|
|
7
|
-
export { bindHttpServer, defineHttpContract, defineHttpImplementation, defineHttpMiddleware, httpError, executionHttp as http, httpExecutionExtension, HTTP_SERVER, validate, } from './extension.js';
|
|
8
|
-
export type { HttpContract, HttpContractBranchDefinition, HttpContractNodeDefinition, HttpContractRouteTree, HttpExecutionContext, HttpExecutionDefinition, HttpExecutionRequestDefinition, HttpExecutionResponseDefinition, HttpExecutionResult, HttpExecutionRouteDefinition, HttpErrorMapping, HttpErrorMatcher, HttpExtensionRuntime, HttpHandlers, HttpHeaderValue, HttpHeaders, HttpHostApi, HttpImplementationDefinition, HttpMiddleware, HttpMiddlewareContext, HttpResponseHeadersDefinition, HttpResponseResult, HttpResponseHeadersWithDefaults, HttpServerDriver, HttpValidationMiddleware, HttpValidationPart, ResolvedHttpContractRoutes, } from './extension.js';
|
|
7
|
+
export { bindHttpServer, defineHttpContract, defineHttpImplementation, defineHttpMiddleware, defineHttpRaw, httpError, executionHttp as http, httpExecutionExtension, isHttpContract, HTTP_SERVER, validate, } from './extension.js';
|
|
8
|
+
export type { HttpRawDefinition, HttpRawRouteDefinition, HttpRawExecutionContext, HttpRawExecutionDefinition, HttpContract, HttpContractBranchDefinition, HttpContractNodeDefinition, HttpContractRouteTree, HttpExecutionContext, HttpExecutionDefinition, HttpExecutionRequestDefinition, HttpExecutionResponseDefinition, HttpExecutionResult, HttpExecutionRouteDefinition, HttpErrorMapping, HttpErrorMatcher, HttpExtensionRuntime, HttpHandlers, HttpHeaderValue, HttpHeaders, HttpHostApi, HttpImplementationDefinition, HttpMiddleware, HttpMiddlewareContext, HttpResponseHeadersDefinition, HttpResponseResult, HttpResponseHeadersWithDefaults, HttpServerDriver, HttpValidationMiddleware, HttpValidationPart, ResolvedHttpContractRoutes, } from './extension.js';
|
|
9
9
|
export { compareHttpPathSpecificity, HttpPathDecodeError, matchHttpPath, normalizeHttpPath, parseHttpPath, } from './path.js';
|
|
10
10
|
export type { HttpPathSegment, PathParamNames } from './path.js';
|
|
11
|
+
export type { HttpRequestHeadDefinition } from './request-head.js';
|
package/dist/http/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createHttpClient, fetchHttpTransport, HttpClientResponseError, } from './client.js';
|
|
2
2
|
export { cors } from './cors.js';
|
|
3
3
|
export { basicAuth, bearerAuth } from './auth.js';
|
|
4
|
-
export { bindHttpServer, defineHttpContract, defineHttpImplementation, defineHttpMiddleware, httpError, executionHttp as http, httpExecutionExtension, HTTP_SERVER, validate, } from './extension.js';
|
|
4
|
+
export { bindHttpServer, defineHttpContract, defineHttpImplementation, defineHttpMiddleware, defineHttpRaw, httpError, executionHttp as http, httpExecutionExtension, isHttpContract, HTTP_SERVER, validate, } from './extension.js';
|
|
5
5
|
export { compareHttpPathSpecificity, HttpPathDecodeError, matchHttpPath, normalizeHttpPath, parseHttpPath, } from './path.js';
|
package/dist/http/openapi.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ApplicationModel } from '../core/index.js';
|
|
2
|
-
import { type HttpExecutionResponseDefinition, type HttpExecutionRouteDefinition } from './extension.js';
|
|
2
|
+
import { type HttpContract, type HttpExecutionResponseDefinition, type HttpExecutionRouteDefinition } from './extension.js';
|
|
3
3
|
export interface OpenApiInfo {
|
|
4
4
|
readonly title: string;
|
|
5
5
|
readonly version: string;
|
|
@@ -37,7 +37,6 @@ export interface HttpOperationTarget {
|
|
|
37
37
|
};
|
|
38
38
|
readonly procedure: string;
|
|
39
39
|
}
|
|
40
|
-
export
|
|
41
|
-
|
|
42
|
-
}, options: GenerateOpenApiOptions): OpenApiDocument;
|
|
40
|
+
export type OpenApiSource = ApplicationModel | HttpContract;
|
|
41
|
+
export declare function generateOpenApi(source: OpenApiSource, options: GenerateOpenApiOptions): OpenApiDocument;
|
|
43
42
|
export {};
|
package/dist/http/openapi.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { supportsJsonSchema, assertValidApplicationModel, } from '../core/index.js';
|
|
2
|
-
import { httpExecutionExtension, } from './extension.js';
|
|
2
|
+
import { httpExecutionExtension, isHttpContract, } from './extension.js';
|
|
3
|
+
import { findHttpRouteConflicts } from './route-validation.js';
|
|
4
|
+
import { match } from 'ts-pattern';
|
|
3
5
|
const FIXED_METHODS = new Set([
|
|
4
6
|
'get',
|
|
5
7
|
'put',
|
|
@@ -12,24 +14,11 @@ const FIXED_METHODS = new Set([
|
|
|
12
14
|
'query',
|
|
13
15
|
]);
|
|
14
16
|
export function generateOpenApi(source, options) {
|
|
15
|
-
const model = 'model' in source ? source.model : source;
|
|
16
|
-
assertValidApplicationModel(model);
|
|
17
17
|
const registry = new SchemaRegistry();
|
|
18
18
|
const paths = {};
|
|
19
19
|
const operationIds = new Set();
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
for (const route of execution.compiled.routes) {
|
|
23
|
-
const target = {
|
|
24
|
-
procedure: route.name,
|
|
25
|
-
definition: {
|
|
26
|
-
...route.definition,
|
|
27
|
-
method: route.method,
|
|
28
|
-
path: route.path,
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
attachOperation(paths, target.definition, createOperation(target, registry, operationIds, options.operationId));
|
|
32
|
-
}
|
|
20
|
+
for (const target of httpOperationTargets(source)) {
|
|
21
|
+
attachOperation(paths, target.definition, createOperation(target, registry, operationIds, options.operationId));
|
|
33
22
|
}
|
|
34
23
|
const schemas = registry.components();
|
|
35
24
|
return {
|
|
@@ -40,6 +29,37 @@ export function generateOpenApi(source, options) {
|
|
|
40
29
|
...(Object.keys(schemas).length === 0 ? {} : { components: { schemas } }),
|
|
41
30
|
};
|
|
42
31
|
}
|
|
32
|
+
function httpOperationTargets(source) {
|
|
33
|
+
if (source.kind === 'http-contract') {
|
|
34
|
+
if (!isHttpContract(source)) {
|
|
35
|
+
throw openApiError('LUTRE_OPENAPI_SOURCE_001', 'HttpContract source must be created by http.contract().');
|
|
36
|
+
}
|
|
37
|
+
const routes = Object.entries(source.routes).map(([procedure, definition]) => ({ procedure, definition }));
|
|
38
|
+
const [conflict] = findHttpRouteConflicts(routes.map(({ definition }) => definition));
|
|
39
|
+
if (conflict) {
|
|
40
|
+
throw openApiError('LUTRE_OPENAPI_OPERATION_001', `Duplicate OpenAPI operation: ${conflict.route.method.toUpperCase()} ${conflict.route.path} conflicts with ${conflict.existing.method.toUpperCase()} ${conflict.existing.path}`);
|
|
41
|
+
}
|
|
42
|
+
return routes.map(({ procedure, definition }) => ({
|
|
43
|
+
procedure,
|
|
44
|
+
definition: {
|
|
45
|
+
...definition,
|
|
46
|
+
method: definition.method.toUpperCase(),
|
|
47
|
+
},
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
assertValidApplicationModel(source);
|
|
51
|
+
const http = source.extensions.get(httpExecutionExtension);
|
|
52
|
+
return (http?.executions ?? []).flatMap((execution) => execution.compiled.routes
|
|
53
|
+
.filter((route) => !route.raw)
|
|
54
|
+
.map((route) => ({
|
|
55
|
+
procedure: route.name,
|
|
56
|
+
definition: {
|
|
57
|
+
...route.definition,
|
|
58
|
+
method: route.method,
|
|
59
|
+
path: route.path,
|
|
60
|
+
},
|
|
61
|
+
})));
|
|
62
|
+
}
|
|
43
63
|
function createOperation(target, registry, operationIds, resolveOperationId) {
|
|
44
64
|
const definition = target.definition;
|
|
45
65
|
const operationId = resolveOperationId?.({
|
|
@@ -244,18 +264,17 @@ function mergeResponseHeaders(entries, registry, target) {
|
|
|
244
264
|
]));
|
|
245
265
|
}
|
|
246
266
|
function responseHeadersSchema(headers) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
return undefined;
|
|
267
|
+
return match(headers)
|
|
268
|
+
.when(isStandardSchema, (schema) => schema)
|
|
269
|
+
.when(isResponseHeadersWithDefaults, (definition) => definition.schema)
|
|
270
|
+
.otherwise(() => undefined);
|
|
252
271
|
}
|
|
253
272
|
function responseHeadersDefaults(headers) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
273
|
+
return match(headers)
|
|
274
|
+
.with(undefined, () => undefined)
|
|
275
|
+
.when(isStandardSchema, () => undefined)
|
|
276
|
+
.when(isResponseHeadersWithDefaults, (definition) => definition.defaults)
|
|
277
|
+
.otherwise((definition) => definition);
|
|
259
278
|
}
|
|
260
279
|
function isStandardSchema(value) {
|
|
261
280
|
return typeof value === 'object' && value !== null && '~standard' in value;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface HttpRouteDescriptor {
|
|
2
|
+
readonly method: string;
|
|
3
|
+
readonly path: string;
|
|
4
|
+
}
|
|
5
|
+
export interface HttpRouteConflict<TRoute extends HttpRouteDescriptor> {
|
|
6
|
+
readonly existing: TRoute;
|
|
7
|
+
readonly route: TRoute;
|
|
8
|
+
}
|
|
9
|
+
export declare function findHttpRouteConflicts<TRoute extends HttpRouteDescriptor>(routes: readonly TRoute[]): readonly HttpRouteConflict<TRoute>[];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createHttpDispatchKey, parseHttpPath } from './path.js';
|
|
2
|
+
export function findHttpRouteConflicts(routes) {
|
|
3
|
+
const dispatches = new Map();
|
|
4
|
+
const conflicts = [];
|
|
5
|
+
for (const route of routes) {
|
|
6
|
+
const dispatch = createHttpDispatchKey(route.method, parseHttpPath(route.path));
|
|
7
|
+
const existing = dispatches.get(dispatch) ??
|
|
8
|
+
[...dispatches.values()].find((candidate) => (route.method === '*' || candidate.method === '*') &&
|
|
9
|
+
createHttpDispatchKey('*', parseHttpPath(candidate.path)) ===
|
|
10
|
+
createHttpDispatchKey('*', parseHttpPath(route.path)));
|
|
11
|
+
if (existing) {
|
|
12
|
+
conflicts.push({ existing, route });
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
dispatches.set(dispatch, route);
|
|
16
|
+
}
|
|
17
|
+
return conflicts;
|
|
18
|
+
}
|