@loutrejs/loutre 0.4.1 → 0.6.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/README.md +63 -32
- package/dist/adapters/aws-lambda.d.ts +5 -3
- package/dist/adapters/aws-lambda.js +53 -23
- package/dist/adapters/bun.d.ts +7 -4
- package/dist/adapters/bun.js +52 -42
- package/dist/adapters/cloudflare-workers.d.ts +5 -3
- package/dist/adapters/cloudflare-workers.js +23 -28
- package/dist/adapters/deno.d.ts +7 -4
- package/dist/adapters/deno.js +94 -57
- package/dist/adapters/electron.d.ts +13 -5
- package/dist/adapters/electron.js +76 -28
- package/dist/application/definition.d.ts +31 -0
- package/dist/application/definition.js +16 -0
- package/dist/application/index.d.ts +4 -63
- package/dist/application/index.js +2 -11
- package/dist/application/kernel-internal.d.ts +2 -0
- package/dist/application/kernel-internal.js +10 -0
- package/dist/application/kernel.d.ts +21 -0
- package/dist/application/kernel.js +65 -0
- package/dist/core/diagnostic.d.ts +9 -0
- package/dist/core/diagnostic.js +9 -0
- package/dist/core/extension.d.ts +107 -0
- package/dist/core/extension.js +73 -0
- package/dist/core/generic-layer.d.ts +26 -0
- package/dist/core/generic-layer.js +116 -0
- package/dist/core/index.d.ts +31 -15
- package/dist/core/index.js +15 -15
- package/dist/core/injection.d.ts +4 -13
- package/dist/core/injection.js +63 -0
- package/dist/core/model/context.d.ts +18 -0
- package/dist/core/model/context.js +34 -0
- package/dist/core/model/lifecycle.d.ts +5 -0
- package/dist/core/model/lifecycle.js +44 -0
- package/dist/core/model/modules.d.ts +2 -0
- package/dist/core/model/modules.js +101 -0
- package/dist/core/model/providers.d.ts +4 -0
- package/dist/core/model/providers.js +74 -0
- package/dist/core/model/snapshot.d.ts +6 -0
- package/dist/core/model/snapshot.js +31 -0
- package/dist/core/model/types.d.ts +78 -0
- package/dist/core/model/validation.d.ts +4 -0
- package/dist/core/model/validation.js +40 -0
- package/dist/core/model/visibility.d.ts +4 -0
- package/dist/core/model/visibility.js +20 -0
- package/dist/core/model.d.ts +10 -0
- package/dist/core/model.js +346 -0
- package/dist/core/module.d.ts +8 -13
- package/dist/core/source-location.d.ts +8 -0
- package/dist/core/source-location.js +31 -0
- package/dist/core/token-internal.d.ts +3 -0
- package/dist/core/token-internal.js +4 -0
- package/dist/core/type.d.ts +7 -0
- package/dist/core/type.js +4 -0
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/graph/index.d.ts +2 -3
- package/dist/graph/index.js +1 -3
- package/dist/graph/model.d.ts +34 -0
- package/dist/graph/model.js +127 -0
- package/dist/http/auth.d.ts +46 -0
- package/dist/http/auth.js +96 -0
- package/dist/http/client.d.ts +28 -31
- package/dist/http/client.js +60 -41
- package/dist/http/cors-internal.d.ts +29 -0
- package/dist/http/cors-internal.js +160 -0
- package/dist/http/cors.d.ts +5 -8
- package/dist/http/cors.js +7 -165
- package/dist/http/extension.d.ts +348 -0
- package/dist/http/extension.js +947 -0
- package/dist/http/index.d.ts +9 -7
- package/dist/http/index.js +5 -7
- package/dist/{application → http}/openapi.d.ts +12 -3
- package/dist/{application → http}/openapi.js +164 -101
- package/dist/http/path.d.ts +2 -39
- package/dist/http/path.js +7 -2
- package/dist/http/runtime/request.d.ts +17 -0
- package/dist/http/runtime/request.js +109 -0
- package/dist/http/runtime/response.d.ts +18 -0
- package/dist/http/runtime/response.js +108 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/message-port/extension.d.ts +66 -0
- package/dist/message-port/extension.js +395 -0
- package/dist/message-port/index.d.ts +4 -73
- package/dist/message-port/index.js +2 -189
- package/dist/message-port/transport.d.ts +11 -0
- package/dist/message-port/transport.js +45 -0
- package/dist/runtime/async-iterator-lifecycle.d.ts +12 -0
- package/dist/runtime/async-iterator-lifecycle.js +95 -0
- package/dist/runtime/capability.d.ts +11 -11
- package/dist/runtime/capability.js +3 -3
- package/dist/runtime/di.d.ts +2 -22
- package/dist/runtime/di.js +8 -285
- package/dist/runtime/execution-tracker.d.ts +8 -0
- package/dist/runtime/execution-tracker.js +54 -0
- package/dist/runtime/index.d.ts +15 -9
- package/dist/runtime/index.js +9 -9
- package/dist/runtime/ingress-gate.d.ts +7 -0
- package/dist/runtime/ingress-gate.js +33 -0
- package/dist/runtime/kernel.d.ts +23 -0
- package/dist/runtime/kernel.js +354 -0
- package/dist/runtime/logger.d.ts +2 -0
- package/dist/runtime/logger.js +2 -0
- package/dist/runtime/node-capabilities.d.ts +1 -1
- package/dist/runtime/node-capabilities.js +1 -1
- package/dist/runtime/server-port.d.ts +2 -0
- package/dist/runtime/server-port.js +11 -0
- package/dist/tasks/extension.d.ts +144 -0
- package/dist/tasks/extension.js +499 -0
- package/dist/tasks/index.d.ts +2 -0
- package/dist/tasks/index.js +1 -0
- package/dist/tasks/runtime/cron.d.ts +6 -0
- package/dist/tasks/runtime/cron.js +89 -0
- package/dist/websocket/errors.d.ts +9 -0
- package/dist/websocket/errors.js +18 -0
- package/dist/websocket/extension.d.ts +154 -0
- package/dist/websocket/extension.js +524 -0
- package/dist/websocket/index.d.ts +3 -0
- package/dist/websocket/index.js +2 -0
- package/package.json +12 -16
- package/dist/application/binding.d.ts +0 -33
- package/dist/application/binding.js +0 -295
- package/dist/application/host.d.ts +0 -8
- package/dist/application/host.js +0 -14
- package/dist/application/queue.d.ts +0 -3
- package/dist/application/queue.js +0 -4
- package/dist/application/scheduler.d.ts +0 -2
- package/dist/application/scheduler.js +0 -45
- package/dist/core/context-key.d.ts +0 -14
- package/dist/core/context-key.js +0 -14
- package/dist/core/contract.d.ts +0 -94
- package/dist/core/contract.js +0 -79
- package/dist/core/execution.d.ts +0 -78
- package/dist/core/execution.js +0 -61
- package/dist/core/implementation.d.ts +0 -34
- package/dist/core/implementation.js +0 -30
- package/dist/core/layer.d.ts +0 -109
- package/dist/core/layer.js +0 -52
- package/dist/graph/compiler.d.ts +0 -4
- package/dist/graph/compiler.js +0 -228
- package/dist/graph/graph.d.ts +0 -17
- package/dist/graph/graph.js +0 -1103
- package/dist/graph/ir.d.ts +0 -175
- package/dist/http/application.d.ts +0 -18
- package/dist/http/application.js +0 -506
- package/dist/http/basic-auth.d.ts +0 -37
- package/dist/http/basic-auth.js +0 -67
- package/dist/http/definitions.d.ts +0 -225
- package/dist/http/definitions.js +0 -69
- package/dist/http/hosted-protocol.d.ts +0 -4
- package/dist/http/hosted-protocol.js +0 -4
- package/dist/http/params.d.ts +0 -2
- package/dist/http/params.js +0 -19
- package/dist/http/request-body.d.ts +0 -4
- package/dist/http/request-body.js +0 -6
- package/dist/http/validate.d.ts +0 -20
- package/dist/http/validate.js +0 -6
- package/dist/message-port/environment.d.ts +0 -2
- package/dist/message-port/environment.js +0 -4
- package/dist/runtime/application.d.ts +0 -38
- package/dist/runtime/application.js +0 -339
- package/dist/runtime/pipeline.d.ts +0 -11
- package/dist/runtime/pipeline.js +0 -161
- /package/dist/{graph/ir.js → core/model/types.js} +0 -0
package/dist/http/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
1
|
+
export { createHttpClient, fetchHttpTransport, HttpClientResponseError, } from './client.js';
|
|
2
|
+
export type { FetchHttpTransportOptions, HttpClient, HttpClientRequest, HttpClientResponse, HttpClientTransport, HttpClientTransportRequest, HttpClientTransportResponse, } from './client.js';
|
|
3
|
+
export { cors } from './cors.js';
|
|
4
4
|
export type { CorsLayerDescriptor, CorsOptions, CorsOrigin } from './cors.js';
|
|
5
|
-
export
|
|
6
|
-
export {
|
|
7
|
-
export
|
|
8
|
-
export {
|
|
5
|
+
export { basicAuth, bearerAuth } from './auth.js';
|
|
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';
|
|
9
|
+
export { compareHttpPathSpecificity, HttpPathDecodeError, matchHttpPath, normalizeHttpPath, parseHttpPath, } from './path.js';
|
|
10
|
+
export type { HttpPathSegment, PathParamNames } from './path.js';
|
package/dist/http/index.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export
|
|
5
|
-
export {
|
|
6
|
-
export * from './request-body.js';
|
|
7
|
-
export { validate } from './validate.js';
|
|
1
|
+
export { createHttpClient, fetchHttpTransport, HttpClientResponseError, } from './client.js';
|
|
2
|
+
export { cors } from './cors.js';
|
|
3
|
+
export { basicAuth, bearerAuth } from './auth.js';
|
|
4
|
+
export { bindHttpServer, defineHttpContract, defineHttpImplementation, defineHttpMiddleware, httpError, executionHttp as http, httpExecutionExtension, HTTP_SERVER, validate, } from './extension.js';
|
|
5
|
+
export { compareHttpPathSpecificity, HttpPathDecodeError, matchHttpPath, normalizeHttpPath, parseHttpPath, } from './path.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ApplicationModel } from '../core/index.js';
|
|
2
|
+
import { type HttpExecutionResponseDefinition, type HttpExecutionRouteDefinition } from './extension.js';
|
|
2
3
|
export interface OpenApiInfo {
|
|
3
4
|
readonly title: string;
|
|
4
5
|
readonly version: string;
|
|
@@ -20,7 +21,6 @@ export interface GenerateOpenApiOptions {
|
|
|
20
21
|
}
|
|
21
22
|
export interface OpenApiDocument {
|
|
22
23
|
readonly openapi: '3.2.0';
|
|
23
|
-
readonly jsonSchemaDialect: 'https://json-schema.org/draft/2020-12/schema';
|
|
24
24
|
readonly info: OpenApiInfo;
|
|
25
25
|
readonly servers?: readonly OpenApiServer[];
|
|
26
26
|
readonly paths: Readonly<Record<string, OpenApiPathItem>>;
|
|
@@ -30,5 +30,14 @@ export interface OpenApiDocument {
|
|
|
30
30
|
}
|
|
31
31
|
type JsonSchema = Record<string, unknown>;
|
|
32
32
|
type OpenApiPathItem = Record<string, unknown>;
|
|
33
|
-
|
|
33
|
+
type OpenApiResponseDefinition = HttpExecutionResponseDefinition;
|
|
34
|
+
export interface HttpOperationTarget {
|
|
35
|
+
readonly definition: Omit<HttpExecutionRouteDefinition, 'responses'> & {
|
|
36
|
+
readonly responses: Readonly<Record<string, OpenApiResponseDefinition>>;
|
|
37
|
+
};
|
|
38
|
+
readonly procedure: string;
|
|
39
|
+
}
|
|
40
|
+
export declare function generateOpenApi(source: ApplicationModel | {
|
|
41
|
+
readonly model: ApplicationModel;
|
|
42
|
+
}, options: GenerateOpenApiOptions): OpenApiDocument;
|
|
34
43
|
export {};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { supportsJsonSchema, } from '../core/index.js';
|
|
2
|
-
import {
|
|
3
|
-
import { httpRequestBodyContentType, httpRequestBodySchema, } from '../http/index.js';
|
|
1
|
+
import { supportsJsonSchema, assertValidApplicationModel, } from '../core/index.js';
|
|
2
|
+
import { httpExecutionExtension, } from './extension.js';
|
|
4
3
|
const FIXED_METHODS = new Set([
|
|
5
4
|
'get',
|
|
6
5
|
'put',
|
|
@@ -12,40 +11,29 @@ const FIXED_METHODS = new Set([
|
|
|
12
11
|
'trace',
|
|
13
12
|
'query',
|
|
14
13
|
]);
|
|
15
|
-
export function generateOpenApi(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
...(application.arguments === undefined
|
|
19
|
-
? {}
|
|
20
|
-
: { arguments: application.arguments }),
|
|
21
|
-
tasks: application.tasks,
|
|
22
|
-
triggers: application.triggers,
|
|
23
|
-
}));
|
|
14
|
+
export function generateOpenApi(source, options) {
|
|
15
|
+
const model = 'model' in source ? source.model : source;
|
|
16
|
+
assertValidApplicationModel(model);
|
|
24
17
|
const registry = new SchemaRegistry();
|
|
25
18
|
const paths = {};
|
|
26
19
|
const operationIds = new Set();
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
};
|
|
40
|
-
const operation = createOperation(target, registry, operationIds, options.operationId);
|
|
41
|
-
attachOperation(paths, typed.definition, operation);
|
|
42
|
-
}
|
|
20
|
+
const http = model.extensions.get(httpExecutionExtension);
|
|
21
|
+
for (const execution of http?.executions ?? []) {
|
|
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));
|
|
43
32
|
}
|
|
44
33
|
}
|
|
45
34
|
const schemas = registry.components();
|
|
46
35
|
return {
|
|
47
36
|
openapi: '3.2.0',
|
|
48
|
-
jsonSchemaDialect: 'https://json-schema.org/draft/2020-12/schema',
|
|
49
37
|
info: options.info,
|
|
50
38
|
...(options.servers === undefined ? {} : { servers: options.servers }),
|
|
51
39
|
paths,
|
|
@@ -82,6 +70,7 @@ function createOperation(target, registry, operationIds, resolveOperationId) {
|
|
|
82
70
|
}
|
|
83
71
|
if (request?.query) {
|
|
84
72
|
parameters.push({
|
|
73
|
+
name: 'query',
|
|
85
74
|
in: 'querystring',
|
|
86
75
|
content: {
|
|
87
76
|
'application/x-www-form-urlencoded': {
|
|
@@ -94,7 +83,7 @@ function createOperation(target, registry, operationIds, resolveOperationId) {
|
|
|
94
83
|
parameters.push(...headerParameters(request.headers, registry, target, 'RequestHeaders_Input'));
|
|
95
84
|
}
|
|
96
85
|
const requestBody = request?.body
|
|
97
|
-
? createRequestBody(request.body, registry, target)
|
|
86
|
+
? createRequestBody(request.body, request.headers, registry, target)
|
|
98
87
|
: undefined;
|
|
99
88
|
return {
|
|
100
89
|
...(definition.tags === undefined ? {} : { tags: definition.tags }),
|
|
@@ -113,21 +102,62 @@ function createOperation(target, registry, operationIds, resolveOperationId) {
|
|
|
113
102
|
: { deprecated: definition.deprecated }),
|
|
114
103
|
};
|
|
115
104
|
}
|
|
116
|
-
function createRequestBody(body, registry, target) {
|
|
117
|
-
const
|
|
105
|
+
function createRequestBody(body, headers, registry, target) {
|
|
106
|
+
const mediaTypes = requestBodyMediaTypes(headers, registry, target);
|
|
107
|
+
const schema = registry.reference(body, 'input', componentName(target, 'RequestBody_Input'));
|
|
118
108
|
return {
|
|
119
|
-
content: {
|
|
120
|
-
[contentType]: {
|
|
121
|
-
schema: registry.reference(httpRequestBodySchema(body), 'input', componentName(target, 'RequestBody_Input')),
|
|
122
|
-
},
|
|
123
|
-
},
|
|
109
|
+
content: Object.fromEntries(mediaTypes.map((mediaType) => [mediaType, { schema }])),
|
|
124
110
|
};
|
|
125
111
|
}
|
|
112
|
+
function requestBodyMediaTypes(headers, registry, target) {
|
|
113
|
+
if (!headers) {
|
|
114
|
+
throw openApiError('LUTRE_OPENAPI_CONTENT_TYPE_001', `${describeTarget(target)} request body requires a request headers schema with content-type.`);
|
|
115
|
+
}
|
|
116
|
+
const materialized = registry.materialize(headers, 'input', componentName(target, 'RequestHeaders_Input'));
|
|
117
|
+
const properties = objectProperties(materialized.schema, 'LUTRE_OPENAPI_HEADER_SCHEMA_001', `${describeTarget(target)} request headers`);
|
|
118
|
+
const contentType = properties['content-type'];
|
|
119
|
+
if (!contentType) {
|
|
120
|
+
throw openApiError('LUTRE_OPENAPI_CONTENT_TYPE_001', `${describeTarget(target)} request headers must declare content-type.`);
|
|
121
|
+
}
|
|
122
|
+
const values = finiteStringValues(contentType);
|
|
123
|
+
if (!values || values.length === 0) {
|
|
124
|
+
throw openApiError('LUTRE_OPENAPI_CONTENT_TYPE_002', `${describeTarget(target)} request content-type must resolve to a finite set of string literals.`);
|
|
125
|
+
}
|
|
126
|
+
return [...new Set(values)];
|
|
127
|
+
}
|
|
128
|
+
function finiteStringValues(schema) {
|
|
129
|
+
if (typeof schema.const === 'string')
|
|
130
|
+
return [schema.const];
|
|
131
|
+
if (Array.isArray(schema.enum)) {
|
|
132
|
+
return schema.enum.every((value) => typeof value === 'string')
|
|
133
|
+
? schema.enum
|
|
134
|
+
: undefined;
|
|
135
|
+
}
|
|
136
|
+
for (const keyword of ['anyOf', 'oneOf']) {
|
|
137
|
+
const alternatives = schema[keyword];
|
|
138
|
+
if (!Array.isArray(alternatives))
|
|
139
|
+
continue;
|
|
140
|
+
const values = [];
|
|
141
|
+
for (const alternative of alternatives) {
|
|
142
|
+
if (typeof alternative !== 'object' ||
|
|
143
|
+
alternative === null ||
|
|
144
|
+
Array.isArray(alternative)) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
const nested = finiteStringValues(alternative);
|
|
148
|
+
if (!nested)
|
|
149
|
+
return undefined;
|
|
150
|
+
values.push(...nested);
|
|
151
|
+
}
|
|
152
|
+
return values;
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
126
156
|
function createResponses(target, registry) {
|
|
127
157
|
const grouped = new Map();
|
|
128
|
-
for (const [
|
|
158
|
+
for (const [name, response] of Object.entries(target.definition.responses)) {
|
|
129
159
|
const current = grouped.get(response.status) ?? [];
|
|
130
|
-
current.push({
|
|
160
|
+
current.push({ name, response });
|
|
131
161
|
grouped.set(response.status, current);
|
|
132
162
|
}
|
|
133
163
|
return Object.fromEntries([...grouped.entries()]
|
|
@@ -142,50 +172,64 @@ function createResponse(target, entries, registry) {
|
|
|
142
172
|
if (streamKinds.size > 1) {
|
|
143
173
|
throw openApiError('LUTRE_OPENAPI_RESPONSE_STREAM_001', `${describeTarget(target)} declares streaming and non-streaming responses with the same status.`);
|
|
144
174
|
}
|
|
145
|
-
const schemas = entries
|
|
175
|
+
const schemas = entries
|
|
176
|
+
.filter(({ response }) => response.body !== undefined)
|
|
177
|
+
.map(({ name, response }) => registry.reference(response.body, 'output', componentName(target, `Response_${name}_Output`)));
|
|
146
178
|
const payloadSchema = schemas.length === 1 ? schemas[0] : { oneOf: schemas };
|
|
147
179
|
const streaming = entries[0]?.response.stream === 'server';
|
|
148
180
|
const headers = mergeResponseHeaders(entries, registry, target);
|
|
149
181
|
const descriptions = [
|
|
150
|
-
...new Set(entries.map(({
|
|
182
|
+
...new Set(entries.map(({ name, response }) => response.description ?? name)),
|
|
151
183
|
];
|
|
152
184
|
return {
|
|
153
185
|
description: descriptions.join(' / '),
|
|
154
186
|
...(headers === undefined ? {} : { headers }),
|
|
155
|
-
|
|
156
|
-
? {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
187
|
+
...(schemas.length === 0
|
|
188
|
+
? {}
|
|
189
|
+
: {
|
|
190
|
+
content: streaming
|
|
191
|
+
? {
|
|
192
|
+
'text/event-stream': {
|
|
193
|
+
itemSchema: {
|
|
194
|
+
type: 'object',
|
|
195
|
+
required: ['data'],
|
|
196
|
+
properties: {
|
|
197
|
+
data: {
|
|
198
|
+
type: 'string',
|
|
199
|
+
contentMediaType: 'application/json',
|
|
200
|
+
contentSchema: payloadSchema,
|
|
201
|
+
},
|
|
202
|
+
},
|
|
166
203
|
},
|
|
167
204
|
},
|
|
205
|
+
}
|
|
206
|
+
: {
|
|
207
|
+
'application/json': {
|
|
208
|
+
schema: payloadSchema,
|
|
209
|
+
},
|
|
168
210
|
},
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
: {
|
|
172
|
-
'application/json': {
|
|
173
|
-
schema: payloadSchema,
|
|
174
|
-
},
|
|
175
|
-
},
|
|
211
|
+
}),
|
|
176
212
|
};
|
|
177
213
|
}
|
|
178
214
|
function mergeResponseHeaders(entries, registry, target) {
|
|
179
215
|
const result = new Map();
|
|
180
|
-
for (const {
|
|
181
|
-
|
|
216
|
+
for (const { name, response } of entries) {
|
|
217
|
+
for (const [headerName, value] of Object.entries(responseHeadersDefaults(response.headers) ?? {})) {
|
|
218
|
+
if (value === undefined)
|
|
219
|
+
continue;
|
|
220
|
+
const current = result.get(headerName) ?? [];
|
|
221
|
+
current.push(responseHeaderDefaultSchema(value));
|
|
222
|
+
result.set(headerName, current);
|
|
223
|
+
}
|
|
224
|
+
const headerSchema = responseHeadersSchema(response.headers);
|
|
225
|
+
if (!headerSchema)
|
|
182
226
|
continue;
|
|
183
|
-
const materialized = registry.materialize(
|
|
227
|
+
const materialized = registry.materialize(headerSchema, 'output', componentName(target, `Response_${name}_Headers_Output`));
|
|
184
228
|
const properties = objectProperties(materialized.schema, 'LUTRE_OPENAPI_HEADER_SCHEMA_001', `${describeTarget(target)} response headers`);
|
|
185
|
-
for (const [
|
|
186
|
-
const current = result.get(
|
|
229
|
+
for (const [headerName, schema] of Object.entries(properties)) {
|
|
230
|
+
const current = result.get(headerName) ?? [];
|
|
187
231
|
current.push(schema);
|
|
188
|
-
result.set(
|
|
232
|
+
result.set(headerName, current);
|
|
189
233
|
}
|
|
190
234
|
}
|
|
191
235
|
if (result.size === 0)
|
|
@@ -199,13 +243,47 @@ function mergeResponseHeaders(entries, registry, target) {
|
|
|
199
243
|
},
|
|
200
244
|
]));
|
|
201
245
|
}
|
|
246
|
+
function responseHeadersSchema(headers) {
|
|
247
|
+
if (isStandardSchema(headers))
|
|
248
|
+
return headers;
|
|
249
|
+
if (isResponseHeadersWithDefaults(headers))
|
|
250
|
+
return headers.schema;
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
function responseHeadersDefaults(headers) {
|
|
254
|
+
if (headers === undefined || isStandardSchema(headers))
|
|
255
|
+
return undefined;
|
|
256
|
+
if (isResponseHeadersWithDefaults(headers))
|
|
257
|
+
return headers.defaults;
|
|
258
|
+
return headers;
|
|
259
|
+
}
|
|
260
|
+
function isStandardSchema(value) {
|
|
261
|
+
return typeof value === 'object' && value !== null && '~standard' in value;
|
|
262
|
+
}
|
|
263
|
+
function isResponseHeadersWithDefaults(value) {
|
|
264
|
+
return (typeof value === 'object' &&
|
|
265
|
+
value !== null &&
|
|
266
|
+
'schema' in value &&
|
|
267
|
+
isStandardSchema(value.schema));
|
|
268
|
+
}
|
|
269
|
+
function responseHeaderDefaultSchema(value) {
|
|
270
|
+
if (typeof value === 'string')
|
|
271
|
+
return { type: 'string', const: value };
|
|
272
|
+
return {
|
|
273
|
+
type: 'array',
|
|
274
|
+
items: { type: 'string' },
|
|
275
|
+
const: [...value],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
202
278
|
function headerParameters(schema, registry, target, suffix) {
|
|
203
279
|
const materialized = registry.materialize(schema, 'input', componentName(target, suffix));
|
|
204
280
|
const properties = objectProperties(materialized.schema, 'LUTRE_OPENAPI_HEADER_SCHEMA_001', `${describeTarget(target)} request headers`);
|
|
205
281
|
const required = new Set(Array.isArray(materialized.schema.required)
|
|
206
282
|
? materialized.schema.required.filter((value) => typeof value === 'string')
|
|
207
283
|
: []);
|
|
208
|
-
return Object.entries(properties)
|
|
284
|
+
return Object.entries(properties)
|
|
285
|
+
.filter(([name]) => name.toLowerCase() !== 'content-type')
|
|
286
|
+
.map(([name, propertySchema]) => ({
|
|
209
287
|
name,
|
|
210
288
|
in: 'header',
|
|
211
289
|
required: required.has(name),
|
|
@@ -240,20 +318,27 @@ function attachOperation(paths, definition, operation) {
|
|
|
240
318
|
}
|
|
241
319
|
additional[methodName] = operation;
|
|
242
320
|
}
|
|
243
|
-
function
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (
|
|
248
|
-
return;
|
|
249
|
-
|
|
250
|
-
for (const
|
|
251
|
-
|
|
252
|
-
|
|
321
|
+
function rebaseLocalDefinitions(schema, schemaComponentName) {
|
|
322
|
+
const visit = (value) => {
|
|
323
|
+
if (Array.isArray(value))
|
|
324
|
+
return value.map(visit);
|
|
325
|
+
if (typeof value !== 'object' || value === null)
|
|
326
|
+
return value;
|
|
327
|
+
const result = {};
|
|
328
|
+
for (const [key, child] of Object.entries(value)) {
|
|
329
|
+
if (key === '$ref' &&
|
|
330
|
+
typeof child === 'string' &&
|
|
331
|
+
child.startsWith('#/$defs/')) {
|
|
332
|
+
result[key] =
|
|
333
|
+
`#/components/schemas/${escapeJsonPointer(schemaComponentName)}/$defs/${child.slice('#/$defs/'.length)}`;
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
result[key] = visit(child);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return result;
|
|
253
340
|
};
|
|
254
|
-
|
|
255
|
-
visit(root);
|
|
256
|
-
return result;
|
|
341
|
+
return visit(schema);
|
|
257
342
|
}
|
|
258
343
|
class SchemaRegistry {
|
|
259
344
|
bySchema = new Map();
|
|
@@ -294,28 +379,6 @@ class SchemaRegistry {
|
|
|
294
379
|
return this.schemas;
|
|
295
380
|
}
|
|
296
381
|
}
|
|
297
|
-
function rebaseLocalDefinitions(schema, schemaComponentName) {
|
|
298
|
-
const visit = (value) => {
|
|
299
|
-
if (Array.isArray(value))
|
|
300
|
-
return value.map(visit);
|
|
301
|
-
if (typeof value !== 'object' || value === null)
|
|
302
|
-
return value;
|
|
303
|
-
const result = {};
|
|
304
|
-
for (const [key, child] of Object.entries(value)) {
|
|
305
|
-
if (key === '$ref' &&
|
|
306
|
-
typeof child === 'string' &&
|
|
307
|
-
child.startsWith('#/$defs/')) {
|
|
308
|
-
result[key] =
|
|
309
|
-
`#/components/schemas/${escapeJsonPointer(schemaComponentName)}/$defs/${child.slice('#/$defs/'.length)}`;
|
|
310
|
-
}
|
|
311
|
-
else {
|
|
312
|
-
result[key] = visit(child);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
return result;
|
|
316
|
-
};
|
|
317
|
-
return visit(schema);
|
|
318
|
-
}
|
|
319
382
|
function componentName(target, suffix) {
|
|
320
383
|
return sanitizeComponentName(`${target.definition.method} ${target.definition.path} ${target.procedure} ${suffix}`);
|
|
321
384
|
}
|
package/dist/http/path.d.ts
CHANGED
|
@@ -1,41 +1,3 @@
|
|
|
1
|
-
type AsciiLowercase = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';
|
|
2
|
-
type AsciiLetter = AsciiLowercase | Uppercase<AsciiLowercase>;
|
|
3
|
-
type AsciiDigit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
|
|
4
|
-
type ParamNameStart = AsciiLetter | '_';
|
|
5
|
-
type ParamNameContinuation = ParamNameStart | AsciiDigit;
|
|
6
|
-
type IsParamNameRest<TName extends string> = TName extends '' ? true : TName extends `${ParamNameContinuation}${infer TRest}` ? IsParamNameRest<TRest> : false;
|
|
7
|
-
type IsParamName<TName extends string> = TName extends `${ParamNameStart}${infer TRest}` ? IsParamNameRest<TRest> : false;
|
|
8
|
-
type IsStaticSegment<TSegment extends string> = TSegment extends '' ? false : TSegment extends `${string}{${string}` | `${string}}${string}` ? false : TSegment extends `${string}?${string}` | `${string}#${string}` ? false : true;
|
|
9
|
-
type ValidatePathSegments<TSegments extends string, TSeen extends string = never> = TSegments extends `${infer THead}/${infer TTail}` ? ValidatePathSegment<THead, TSeen> extends infer TResult ? TResult extends {
|
|
10
|
-
readonly valid: true;
|
|
11
|
-
readonly seen: infer TNext extends string;
|
|
12
|
-
} ? ValidatePathSegments<TTail, TNext> : false : false : ValidatePathSegment<TSegments, TSeen> extends {
|
|
13
|
-
readonly valid: true;
|
|
14
|
-
} ? true : false;
|
|
15
|
-
type ValidatePathSegment<TSegment extends string, TSeen extends string> = TSegment extends `{${infer TName}}` ? IsParamName<TName> extends true ? TName extends TSeen ? {
|
|
16
|
-
readonly valid: false;
|
|
17
|
-
} : {
|
|
18
|
-
readonly valid: true;
|
|
19
|
-
readonly seen: TSeen | TName;
|
|
20
|
-
} : {
|
|
21
|
-
readonly valid: false;
|
|
22
|
-
} : IsStaticSegment<TSegment> extends true ? {
|
|
23
|
-
readonly valid: true;
|
|
24
|
-
readonly seen: TSeen;
|
|
25
|
-
} : {
|
|
26
|
-
readonly valid: false;
|
|
27
|
-
};
|
|
28
|
-
export type IsValidHttpPath<TPath extends string> = string extends TPath ? false : TPath extends '/' ? true : TPath extends `/${infer TSegments}` ? ValidatePathSegments<TSegments> : false;
|
|
29
|
-
type PathParamNamesFromSegments<TSegments extends string> = TSegments extends `${infer THead}/${infer TTail}` ? PathParamName<THead> | PathParamNamesFromSegments<TTail> : PathParamName<TSegments>;
|
|
30
|
-
type PathParamName<TSegment extends string> = TSegment extends `{${infer TName}}` ? TName : never;
|
|
31
|
-
export type PathParamNames<TPath extends string> = TPath extends `/${infer TSegments}` ? PathParamNamesFromSegments<TSegments> : never;
|
|
32
|
-
export type RawPathParams<TPath extends string> = Readonly<{
|
|
33
|
-
[TName in PathParamNames<TPath>]: string;
|
|
34
|
-
}>;
|
|
35
|
-
type NormalizeHttpPathSegments<TSegments extends string> = TSegments extends `${infer THead}/${infer TTail}` ? `${NormalizeHttpPathSegment<THead>}/${NormalizeHttpPathSegments<TTail>}` : NormalizeHttpPathSegment<TSegments>;
|
|
36
|
-
type NormalizeHttpPathSegment<TSegment extends string> = TSegment extends `{${string}}` ? '{}' : TSegment;
|
|
37
|
-
export type NormalizeHttpPath<TPath extends string> = TPath extends '/' ? '/' : string extends TPath ? string : TPath extends `/${infer TSegments}` ? `/${NormalizeHttpPathSegments<TSegments>}` : never;
|
|
38
|
-
export type HttpDispatchKey<TMethod extends string, TPath extends string> = `http:${Uppercase<TMethod>}:${NormalizeHttpPath<TPath>}`;
|
|
39
1
|
export type HttpPathSegment = {
|
|
40
2
|
readonly kind: 'static';
|
|
41
3
|
readonly value: string;
|
|
@@ -43,6 +5,8 @@ export type HttpPathSegment = {
|
|
|
43
5
|
readonly kind: 'param';
|
|
44
6
|
readonly name: string;
|
|
45
7
|
};
|
|
8
|
+
export type PathParamNames<TPath extends string> = TPath extends `${string}{${infer TName}}${infer TRest}` ? TName | PathParamNames<TRest> : never;
|
|
9
|
+
export declare function assertValidHttpMethod(method: string): void;
|
|
46
10
|
export declare function parseHttpPath(path: string): readonly HttpPathSegment[];
|
|
47
11
|
export declare function normalizeHttpPath(segments: readonly HttpPathSegment[]): string;
|
|
48
12
|
export declare function createHttpDispatchKey(method: string, segments: readonly HttpPathSegment[]): string;
|
|
@@ -52,4 +16,3 @@ export declare class HttpPathDecodeError extends Error {
|
|
|
52
16
|
readonly cause: unknown;
|
|
53
17
|
constructor(cause: unknown);
|
|
54
18
|
}
|
|
55
|
-
export {};
|
package/dist/http/path.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
const PARAM_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2
|
+
const HTTP_METHOD_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
3
|
+
export function assertValidHttpMethod(method) {
|
|
4
|
+
if (!HTTP_METHOD_PATTERN.test(method)) {
|
|
5
|
+
throw new Error(`Invalid HTTP method: ${JSON.stringify(method)}`);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
2
8
|
export function parseHttpPath(path) {
|
|
3
9
|
if (path === '/')
|
|
4
10
|
return Object.freeze([]);
|
|
@@ -50,8 +56,7 @@ export function matchHttpPath(segments, pathname) {
|
|
|
50
56
|
if (requestSegments.length !== segments.length)
|
|
51
57
|
return undefined;
|
|
52
58
|
const params = {};
|
|
53
|
-
for (
|
|
54
|
-
const routeSegment = segments[index];
|
|
59
|
+
for (const [index, routeSegment] of segments.entries()) {
|
|
55
60
|
const requestSegment = requestSegments[index];
|
|
56
61
|
if (routeSegment.kind === 'static') {
|
|
57
62
|
if (routeSegment.value !== requestSegment)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type StandardSchemaV1 } from '../../core/index.js';
|
|
2
|
+
export declare function decodeQuery(searchParams: URLSearchParams): Readonly<Record<string, string | string[]>>;
|
|
3
|
+
export declare function validateRequestHeaders(schema: StandardSchemaV1, bodyDeclared: boolean, headers: Headers): Promise<unknown>;
|
|
4
|
+
export declare function validatedContentType(headers: unknown): string;
|
|
5
|
+
export declare function decodeBody(request: Request, contentType: string): Promise<unknown>;
|
|
6
|
+
export declare class HttpInputDecodeError extends Error {
|
|
7
|
+
readonly cause: unknown;
|
|
8
|
+
constructor(cause: unknown);
|
|
9
|
+
}
|
|
10
|
+
export declare class HttpInputValidationError extends Error {
|
|
11
|
+
readonly cause: unknown;
|
|
12
|
+
constructor(cause: unknown);
|
|
13
|
+
}
|
|
14
|
+
export declare class HttpUnsupportedMediaTypeError extends Error {
|
|
15
|
+
readonly mediaType: string | undefined;
|
|
16
|
+
constructor(mediaType: string | undefined);
|
|
17
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { SchemaValidationError, validateSchema, } from '../../core/index.js';
|
|
2
|
+
export function decodeQuery(searchParams) {
|
|
3
|
+
const query = {};
|
|
4
|
+
for (const [key, value] of searchParams) {
|
|
5
|
+
const current = query[key];
|
|
6
|
+
query[key] =
|
|
7
|
+
current === undefined
|
|
8
|
+
? value
|
|
9
|
+
: Array.isArray(current)
|
|
10
|
+
? [...current, value]
|
|
11
|
+
: [current, value];
|
|
12
|
+
}
|
|
13
|
+
return query;
|
|
14
|
+
}
|
|
15
|
+
export async function validateRequestHeaders(schema, bodyDeclared, headers) {
|
|
16
|
+
try {
|
|
17
|
+
return await validateSchema(schema, requestHeadersForValidation(headers));
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (bodyDeclared &&
|
|
21
|
+
error instanceof SchemaValidationError &&
|
|
22
|
+
hasContentTypeIssue(error)) {
|
|
23
|
+
throw new HttpUnsupportedMediaTypeError(normalizeMediaType(headers.get('content-type')));
|
|
24
|
+
}
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function validatedContentType(headers) {
|
|
29
|
+
if (typeof headers !== 'object' || headers === null) {
|
|
30
|
+
throw new TypeError('HTTP body requires validated request headers');
|
|
31
|
+
}
|
|
32
|
+
const contentType = headers['content-type'];
|
|
33
|
+
if (typeof contentType !== 'string' || contentType.length === 0) {
|
|
34
|
+
throw new TypeError('HTTP body requires request.headers content-type to resolve to a string');
|
|
35
|
+
}
|
|
36
|
+
return contentType;
|
|
37
|
+
}
|
|
38
|
+
export async function decodeBody(request, contentType) {
|
|
39
|
+
const mediaType = normalizeMediaType(contentType);
|
|
40
|
+
if (mediaType === 'application/json' || mediaType.endsWith('+json')) {
|
|
41
|
+
try {
|
|
42
|
+
return await request.json();
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
throw new HttpInputDecodeError(error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (mediaType === 'multipart/form-data') {
|
|
49
|
+
try {
|
|
50
|
+
return await request.formData();
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
throw new HttpInputDecodeError(error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (mediaType.startsWith('text/')) {
|
|
57
|
+
try {
|
|
58
|
+
return await request.text();
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
throw new HttpInputDecodeError(error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return request.body;
|
|
65
|
+
}
|
|
66
|
+
function requestHeadersForValidation(headers) {
|
|
67
|
+
const decoded = Object.fromEntries(headers.entries());
|
|
68
|
+
const contentType = normalizeMediaType(headers.get('content-type'));
|
|
69
|
+
if (contentType)
|
|
70
|
+
decoded['content-type'] = contentType;
|
|
71
|
+
return decoded;
|
|
72
|
+
}
|
|
73
|
+
function hasContentTypeIssue(error) {
|
|
74
|
+
return error.issues.some((issue) => {
|
|
75
|
+
const first = issue.path?.[0];
|
|
76
|
+
const key = typeof first === 'object' && first !== null && 'key' in first
|
|
77
|
+
? first.key
|
|
78
|
+
: first;
|
|
79
|
+
return key === 'content-type';
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function normalizeMediaType(value) {
|
|
83
|
+
const normalized = value?.split(';', 1)[0]?.trim().toLowerCase();
|
|
84
|
+
return normalized ? normalized : undefined;
|
|
85
|
+
}
|
|
86
|
+
export class HttpInputDecodeError extends Error {
|
|
87
|
+
cause;
|
|
88
|
+
constructor(cause) {
|
|
89
|
+
super('HTTP request body decode failed', { cause });
|
|
90
|
+
this.cause = cause;
|
|
91
|
+
this.name = 'HttpInputDecodeError';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export class HttpInputValidationError extends Error {
|
|
95
|
+
cause;
|
|
96
|
+
constructor(cause) {
|
|
97
|
+
super('HTTP request body validation failed', { cause });
|
|
98
|
+
this.cause = cause;
|
|
99
|
+
this.name = 'HttpInputValidationError';
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export class HttpUnsupportedMediaTypeError extends Error {
|
|
103
|
+
mediaType;
|
|
104
|
+
constructor(mediaType) {
|
|
105
|
+
super('HTTP request content type is unsupported by the Contract');
|
|
106
|
+
this.mediaType = mediaType;
|
|
107
|
+
this.name = 'HttpUnsupportedMediaTypeError';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type StandardSchemaV1 } from '../../core/index.js';
|
|
2
|
+
type HttpHeaderValue = string | readonly string[];
|
|
3
|
+
type HttpHeaders = Readonly<Record<string, HttpHeaderValue | undefined>>;
|
|
4
|
+
interface HttpResponseHeadersWithDefaults {
|
|
5
|
+
readonly schema: StandardSchemaV1;
|
|
6
|
+
readonly defaults: HttpHeaders;
|
|
7
|
+
}
|
|
8
|
+
type HttpResponseHeadersDefinition = StandardSchemaV1 | HttpHeaders | HttpResponseHeadersWithDefaults;
|
|
9
|
+
export declare function validateResponseHeaders(schema: StandardSchemaV1 | undefined, headers: HttpHeaders | undefined): Promise<HttpHeaders | undefined>;
|
|
10
|
+
export declare function responseHeadersSchema(headers: HttpResponseHeadersDefinition | undefined): StandardSchemaV1 | undefined;
|
|
11
|
+
export declare function responseHeadersDefaults(headers: HttpResponseHeadersDefinition | undefined): HttpHeaders | undefined;
|
|
12
|
+
export declare function isStandardSchema(value: unknown): value is StandardSchemaV1;
|
|
13
|
+
export declare function isResponseHeadersWithDefaults(value: unknown): value is HttpResponseHeadersWithDefaults;
|
|
14
|
+
export declare function mergeResponseHeaders(defaults: HttpHeaders | undefined, dynamic: HttpHeaders | undefined): Headers;
|
|
15
|
+
export declare function applyResponseHeaders(headers: Headers, source: HttpHeaders | undefined): void;
|
|
16
|
+
export declare function applyFrameworkHeadersToResponse(response: Response, source: Headers | undefined): Response;
|
|
17
|
+
export declare function applyFrameworkResponseHeaders(headers: Headers, source: HttpHeaders | undefined): void;
|
|
18
|
+
export {};
|