@typespec/http-server-js 0.58.0-alpha.10-dev.3
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/CHANGELOG.md +69 -0
- package/LICENSE +21 -0
- package/README.md +183 -0
- package/build-helpers.ts +170 -0
- package/dist/generated-defs/helpers/header.d.ts +4 -0
- package/dist/generated-defs/helpers/header.d.ts.map +1 -0
- package/dist/generated-defs/helpers/header.js +76 -0
- package/dist/generated-defs/helpers/header.js.map +1 -0
- package/dist/generated-defs/helpers/http.d.ts +4 -0
- package/dist/generated-defs/helpers/http.d.ts.map +1 -0
- package/dist/generated-defs/helpers/http.js +134 -0
- package/dist/generated-defs/helpers/http.js.map +1 -0
- package/dist/generated-defs/helpers/index.d.ts +4 -0
- package/dist/generated-defs/helpers/index.d.ts.map +1 -0
- package/dist/generated-defs/helpers/index.js +21 -0
- package/dist/generated-defs/helpers/index.js.map +1 -0
- package/dist/generated-defs/helpers/multipart.d.ts +4 -0
- package/dist/generated-defs/helpers/multipart.d.ts.map +1 -0
- package/dist/generated-defs/helpers/multipart.js +249 -0
- package/dist/generated-defs/helpers/multipart.js.map +1 -0
- package/dist/generated-defs/helpers/router.d.ts +4 -0
- package/dist/generated-defs/helpers/router.d.ts.map +1 -0
- package/dist/generated-defs/helpers/router.js +259 -0
- package/dist/generated-defs/helpers/router.js.map +1 -0
- package/dist/src/common/declaration.d.ts +13 -0
- package/dist/src/common/declaration.d.ts.map +1 -0
- package/dist/src/common/declaration.js +45 -0
- package/dist/src/common/declaration.js.map +1 -0
- package/dist/src/common/documentation.d.ts +12 -0
- package/dist/src/common/documentation.d.ts.map +1 -0
- package/dist/src/common/documentation.js +21 -0
- package/dist/src/common/documentation.js.map +1 -0
- package/dist/src/common/enum.d.ts +10 -0
- package/dist/src/common/enum.d.ts.map +1 -0
- package/dist/src/common/enum.js +21 -0
- package/dist/src/common/enum.js.map +1 -0
- package/dist/src/common/interface.d.ts +50 -0
- package/dist/src/common/interface.d.ts.map +1 -0
- package/dist/src/common/interface.js +194 -0
- package/dist/src/common/interface.js.map +1 -0
- package/dist/src/common/model.d.ts +26 -0
- package/dist/src/common/model.d.ts.map +1 -0
- package/dist/src/common/model.js +115 -0
- package/dist/src/common/model.js.map +1 -0
- package/dist/src/common/namespace.d.ts +38 -0
- package/dist/src/common/namespace.d.ts.map +1 -0
- package/dist/src/common/namespace.js +184 -0
- package/dist/src/common/namespace.js.map +1 -0
- package/dist/src/common/reference.d.ts +46 -0
- package/dist/src/common/reference.d.ts.map +1 -0
- package/dist/src/common/reference.js +243 -0
- package/dist/src/common/reference.js.map +1 -0
- package/dist/src/common/scalar.d.ts +50 -0
- package/dist/src/common/scalar.d.ts.map +1 -0
- package/dist/src/common/scalar.js +144 -0
- package/dist/src/common/scalar.js.map +1 -0
- package/dist/src/common/serialization/index.d.ts +11 -0
- package/dist/src/common/serialization/index.d.ts.map +1 -0
- package/dist/src/common/serialization/index.js +72 -0
- package/dist/src/common/serialization/index.js.map +1 -0
- package/dist/src/common/serialization/json.d.ts +6 -0
- package/dist/src/common/serialization/json.d.ts.map +1 -0
- package/dist/src/common/serialization/json.js +341 -0
- package/dist/src/common/serialization/json.js.map +1 -0
- package/dist/src/common/union.d.ts +23 -0
- package/dist/src/common/union.d.ts.map +1 -0
- package/dist/src/common/union.js +57 -0
- package/dist/src/common/union.js.map +1 -0
- package/dist/src/ctx.d.ts +242 -0
- package/dist/src/ctx.d.ts.map +1 -0
- package/dist/src/ctx.js +211 -0
- package/dist/src/ctx.js.map +1 -0
- package/dist/src/helpers/header.d.ts +14 -0
- package/dist/src/helpers/header.d.ts.map +1 -0
- package/dist/src/helpers/header.js +38 -0
- package/dist/src/helpers/header.js.map +1 -0
- package/dist/src/helpers/http.d.ts +70 -0
- package/dist/src/helpers/http.d.ts.map +1 -0
- package/dist/src/helpers/http.js +86 -0
- package/dist/src/helpers/http.js.map +1 -0
- package/dist/src/helpers/multipart.d.ts +26 -0
- package/dist/src/helpers/multipart.d.ts.map +1 -0
- package/dist/src/helpers/multipart.js +182 -0
- package/dist/src/helpers/multipart.js.map +1 -0
- package/dist/src/helpers/router.d.ts +176 -0
- package/dist/src/helpers/router.d.ts.map +1 -0
- package/dist/src/helpers/router.js +55 -0
- package/dist/src/helpers/router.js.map +1 -0
- package/dist/src/http/index.d.ts +24 -0
- package/dist/src/http/index.d.ts.map +1 -0
- package/dist/src/http/index.js +52 -0
- package/dist/src/http/index.js.map +1 -0
- package/dist/src/http/server/index.d.ts +11 -0
- package/dist/src/http/server/index.d.ts.map +1 -0
- package/dist/src/http/server/index.js +413 -0
- package/dist/src/http/server/index.js.map +1 -0
- package/dist/src/http/server/multipart.d.ts +16 -0
- package/dist/src/http/server/multipart.d.ts.map +1 -0
- package/dist/src/http/server/multipart.js +214 -0
- package/dist/src/http/server/multipart.js.map +1 -0
- package/dist/src/http/server/router.d.ts +15 -0
- package/dist/src/http/server/router.d.ts.map +1 -0
- package/dist/src/http/server/router.js +459 -0
- package/dist/src/http/server/router.js.map +1 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +38 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lib.d.ts +141 -0
- package/dist/src/lib.d.ts.map +1 -0
- package/dist/src/lib.js +116 -0
- package/dist/src/lib.js.map +1 -0
- package/dist/src/scripts/scaffold/bin.d.mts +14 -0
- package/dist/src/scripts/scaffold/bin.d.mts.map +1 -0
- package/dist/src/scripts/scaffold/bin.mjs +559 -0
- package/dist/src/scripts/scaffold/bin.mjs.map +1 -0
- package/dist/src/testing/index.d.ts +3 -0
- package/dist/src/testing/index.d.ts.map +1 -0
- package/dist/src/testing/index.js +6 -0
- package/dist/src/testing/index.js.map +1 -0
- package/dist/src/util/case.d.ts +81 -0
- package/dist/src/util/case.d.ts.map +1 -0
- package/dist/src/util/case.js +111 -0
- package/dist/src/util/case.js.map +1 -0
- package/dist/src/util/differentiate.d.ts +251 -0
- package/dist/src/util/differentiate.d.ts.map +1 -0
- package/dist/src/util/differentiate.js +580 -0
- package/dist/src/util/differentiate.js.map +1 -0
- package/dist/src/util/error.d.ts +13 -0
- package/dist/src/util/error.d.ts.map +1 -0
- package/dist/src/util/error.js +25 -0
- package/dist/src/util/error.js.map +1 -0
- package/dist/src/util/extends.d.ts +10 -0
- package/dist/src/util/extends.d.ts.map +1 -0
- package/dist/src/util/extends.js +31 -0
- package/dist/src/util/extends.js.map +1 -0
- package/dist/src/util/iter.d.ts +39 -0
- package/dist/src/util/iter.d.ts.map +1 -0
- package/dist/src/util/iter.js +72 -0
- package/dist/src/util/iter.js.map +1 -0
- package/dist/src/util/keywords.d.ts +10 -0
- package/dist/src/util/keywords.d.ts.map +1 -0
- package/dist/src/util/keywords.js +85 -0
- package/dist/src/util/keywords.js.map +1 -0
- package/dist/src/util/name.d.ts +12 -0
- package/dist/src/util/name.d.ts.map +1 -0
- package/dist/src/util/name.js +26 -0
- package/dist/src/util/name.js.map +1 -0
- package/dist/src/util/once-queue.d.ts +24 -0
- package/dist/src/util/once-queue.d.ts.map +1 -0
- package/dist/src/util/once-queue.js +34 -0
- package/dist/src/util/once-queue.js.map +1 -0
- package/dist/src/util/openapi3.d.ts +23 -0
- package/dist/src/util/openapi3.d.ts.map +1 -0
- package/dist/src/util/openapi3.js +40 -0
- package/dist/src/util/openapi3.js.map +1 -0
- package/dist/src/util/pluralism.d.ts +23 -0
- package/dist/src/util/pluralism.d.ts.map +1 -0
- package/dist/src/util/pluralism.js +36 -0
- package/dist/src/util/pluralism.js.map +1 -0
- package/dist/src/util/scope.d.ts +85 -0
- package/dist/src/util/scope.d.ts.map +1 -0
- package/dist/src/util/scope.js +111 -0
- package/dist/src/util/scope.js.map +1 -0
- package/dist/src/write.d.ts +23 -0
- package/dist/src/write.d.ts.map +1 -0
- package/dist/src/write.js +62 -0
- package/dist/src/write.js.map +1 -0
- package/generated-defs/helpers/header.ts +83 -0
- package/generated-defs/helpers/http.ts +141 -0
- package/generated-defs/helpers/index.ts +27 -0
- package/generated-defs/helpers/multipart.ts +256 -0
- package/generated-defs/helpers/router.ts +266 -0
- package/package.json +71 -0
- package/src/common/declaration.ts +52 -0
- package/src/common/documentation.ts +26 -0
- package/src/common/enum.ts +28 -0
- package/src/common/interface.ts +264 -0
- package/src/common/model.ts +160 -0
- package/src/common/namespace.ts +243 -0
- package/src/common/reference.ts +319 -0
- package/src/common/scalar.ts +173 -0
- package/src/common/serialization/index.ts +124 -0
- package/src/common/serialization/json.ts +444 -0
- package/src/common/union.ts +76 -0
- package/src/ctx.ts +497 -0
- package/src/helpers/header.ts +55 -0
- package/src/helpers/http.ts +113 -0
- package/src/helpers/multipart.ts +228 -0
- package/src/helpers/router.ts +238 -0
- package/src/http/index.ts +81 -0
- package/src/http/server/index.ts +548 -0
- package/src/http/server/multipart.ts +272 -0
- package/src/http/server/router.ts +686 -0
- package/src/index.ts +56 -0
- package/src/lib.ts +130 -0
- package/src/scripts/scaffold/bin.mts +781 -0
- package/src/testing/index.ts +10 -0
- package/src/util/case.ts +182 -0
- package/src/util/differentiate.ts +957 -0
- package/src/util/error.ts +28 -0
- package/src/util/extends.ts +43 -0
- package/src/util/iter.ts +85 -0
- package/src/util/keywords.ts +90 -0
- package/src/util/name.ts +33 -0
- package/src/util/once-queue.ts +55 -0
- package/src/util/openapi3.ts +53 -0
- package/src/util/pluralism.ts +37 -0
- package/src/util/scope.ts +211 -0
- package/src/write.ts +88 -0
- package/temp/tsconfig.tsbuildinfo +1 -0
- package/test/header.test.ts +26 -0
- package/test/multipart.test.ts +169 -0
- package/tsconfig.json +10 -0
- package/vitest.config.ts +4 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation
|
|
2
|
+
// Licensed under the MIT license.
|
|
3
|
+
|
|
4
|
+
import { ModelProperty, NoTarget, Type, compilerAssert } from "@typespec/compiler";
|
|
5
|
+
import {
|
|
6
|
+
HttpOperation,
|
|
7
|
+
HttpOperationParameter,
|
|
8
|
+
getHeaderFieldName,
|
|
9
|
+
isBody,
|
|
10
|
+
isHeader,
|
|
11
|
+
isStatusCode,
|
|
12
|
+
} from "@typespec/http";
|
|
13
|
+
import { createOrGetModuleForNamespace } from "../../common/namespace.js";
|
|
14
|
+
import { emitTypeReference, isValueLiteralType } from "../../common/reference.js";
|
|
15
|
+
import { parseTemplateForScalar } from "../../common/scalar.js";
|
|
16
|
+
import {
|
|
17
|
+
SerializableType,
|
|
18
|
+
isSerializationRequired,
|
|
19
|
+
requireSerialization,
|
|
20
|
+
} from "../../common/serialization/index.js";
|
|
21
|
+
import { Module, completePendingDeclarations, createModule } from "../../ctx.js";
|
|
22
|
+
import { isUnspeakable, parseCase } from "../../util/case.js";
|
|
23
|
+
import { UnimplementedError } from "../../util/error.js";
|
|
24
|
+
import { getAllProperties } from "../../util/extends.js";
|
|
25
|
+
import { bifilter, indent } from "../../util/iter.js";
|
|
26
|
+
import { keywordSafe } from "../../util/keywords.js";
|
|
27
|
+
import { HttpContext } from "../index.js";
|
|
28
|
+
|
|
29
|
+
import { module as routerHelpers } from "../../../generated-defs/helpers/router.js";
|
|
30
|
+
import { reportDiagnostic } from "../../lib.js";
|
|
31
|
+
import { differentiateUnion, writeCodeTree } from "../../util/differentiate.js";
|
|
32
|
+
import { emitMultipart, emitMultipartLegacy } from "./multipart.js";
|
|
33
|
+
|
|
34
|
+
import { module as headerHelpers } from "../../../generated-defs/helpers/header.js";
|
|
35
|
+
import { module as httpHelpers } from "../../../generated-defs/helpers/http.js";
|
|
36
|
+
import { requiresJsonSerialization } from "../../common/serialization/json.js";
|
|
37
|
+
|
|
38
|
+
const DEFAULT_CONTENT_TYPE = "application/json";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Emits raw operations for handling incoming server requests.
|
|
42
|
+
*
|
|
43
|
+
* @param ctx - The HTTP emitter context.
|
|
44
|
+
* @param operationsModule - The module to emit the operations into.
|
|
45
|
+
* @returns the module containing the raw server operations.
|
|
46
|
+
*/
|
|
47
|
+
export function emitRawServer(ctx: HttpContext, operationsModule: Module): Module {
|
|
48
|
+
const serverRawModule = createModule("server-raw", operationsModule);
|
|
49
|
+
|
|
50
|
+
serverRawModule.imports.push({
|
|
51
|
+
binder: ["HttpContext"],
|
|
52
|
+
from: routerHelpers,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const isHttpResponder = ctx.gensym("isHttpResponder");
|
|
56
|
+
const httpResponderSym = ctx.gensym("httpResponderSymbol");
|
|
57
|
+
|
|
58
|
+
serverRawModule.imports.push({
|
|
59
|
+
binder: [`isHttpResponder as ${isHttpResponder}`, `HTTP_RESPONDER as ${httpResponderSym}`],
|
|
60
|
+
from: httpHelpers,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
for (const operation of ctx.httpService.operations) {
|
|
64
|
+
serverRawModule.declarations.push([
|
|
65
|
+
...emitRawServerOperation(ctx, operation, serverRawModule, {
|
|
66
|
+
isHttpResponder,
|
|
67
|
+
httpResponderSym,
|
|
68
|
+
}),
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return serverRawModule;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Emit a raw operation handler for a specific operation.
|
|
77
|
+
* @param ctx - The HTTP emitter context.
|
|
78
|
+
* @param operation - The operation to create a handler for.
|
|
79
|
+
* @param module - The module that the handler will be written to.
|
|
80
|
+
*/
|
|
81
|
+
function* emitRawServerOperation(
|
|
82
|
+
ctx: HttpContext,
|
|
83
|
+
operation: HttpOperation,
|
|
84
|
+
module: Module,
|
|
85
|
+
responderNames: Pick<Names, "isHttpResponder" | "httpResponderSym">,
|
|
86
|
+
): Iterable<string> {
|
|
87
|
+
const op = operation.operation;
|
|
88
|
+
const operationNameCase = parseCase(op.name);
|
|
89
|
+
|
|
90
|
+
const container = op.interface ?? op.namespace!;
|
|
91
|
+
const containerNameCase = parseCase(container.name);
|
|
92
|
+
|
|
93
|
+
module.imports.push({
|
|
94
|
+
binder: [containerNameCase.pascalCase],
|
|
95
|
+
from: createOrGetModuleForNamespace(ctx, container.namespace!),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
completePendingDeclarations(ctx);
|
|
99
|
+
|
|
100
|
+
const pathParameters = operation.parameters.parameters.filter(function isPathParameter(param) {
|
|
101
|
+
return param.type === "path";
|
|
102
|
+
}) as Extract<HttpOperationParameter, { type: "path" }>[];
|
|
103
|
+
|
|
104
|
+
const functionName = keywordSafe(containerNameCase.snakeCase + "_" + operationNameCase.snakeCase);
|
|
105
|
+
|
|
106
|
+
const names: Names = {
|
|
107
|
+
ctx: ctx.gensym("ctx"),
|
|
108
|
+
result: ctx.gensym("result"),
|
|
109
|
+
operations: ctx.gensym("operations"),
|
|
110
|
+
queryParams: ctx.gensym("queryParams"),
|
|
111
|
+
...responderNames,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
yield `export async function ${functionName}(`;
|
|
115
|
+
yield ` ${names.ctx}: HttpContext,`;
|
|
116
|
+
yield ` ${names.operations}: ${containerNameCase.pascalCase},`;
|
|
117
|
+
|
|
118
|
+
for (const pathParam of pathParameters) {
|
|
119
|
+
yield ` ${parseCase(pathParam.param.name).camelCase}: string,`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
yield "): Promise<void> {";
|
|
123
|
+
|
|
124
|
+
const [_, parameters] = bifilter(op.parameters.properties.values(), (param) =>
|
|
125
|
+
isValueLiteralType(param.type),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const queryParams: Extract<HttpOperationParameter, { type: "query" }>[] = [];
|
|
129
|
+
|
|
130
|
+
const parsedParams = new Set<ModelProperty>();
|
|
131
|
+
|
|
132
|
+
for (const parameter of operation.parameters.parameters) {
|
|
133
|
+
const resolvedParameter =
|
|
134
|
+
parameter.param.type.kind === "ModelProperty" ? parameter.param.type : parameter.param;
|
|
135
|
+
switch (parameter.type) {
|
|
136
|
+
case "header":
|
|
137
|
+
yield* indent(emitHeaderParamBinding(ctx, operation, names, parameter));
|
|
138
|
+
break;
|
|
139
|
+
case "cookie":
|
|
140
|
+
throw new UnimplementedError("cookie parameters");
|
|
141
|
+
case "query":
|
|
142
|
+
queryParams.push(parameter);
|
|
143
|
+
parsedParams.add(resolvedParameter);
|
|
144
|
+
break;
|
|
145
|
+
case "path":
|
|
146
|
+
// Already handled above.
|
|
147
|
+
parsedParams.add(resolvedParameter);
|
|
148
|
+
break;
|
|
149
|
+
default:
|
|
150
|
+
throw new Error(
|
|
151
|
+
`UNREACHABLE: parameter type ${
|
|
152
|
+
(parameter satisfies never as HttpOperationParameter).type
|
|
153
|
+
}`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (queryParams.length > 0) {
|
|
159
|
+
yield ` const ${names.queryParams} = new URLSearchParams(${names.ctx}.request.url!.split("?", 2)[1] ?? "");`;
|
|
160
|
+
yield "";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
for (const qp of queryParams) {
|
|
164
|
+
yield* indent(emitQueryParamBinding(ctx, operation, names, qp));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const bodyFields = new Map<string, Type>(
|
|
168
|
+
operation.parameters.body && operation.parameters.body.type.kind === "Model"
|
|
169
|
+
? getAllProperties(operation.parameters.body.type).map((p) => [p.name, p.type] as const)
|
|
170
|
+
: [],
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
let bodyName: string | undefined = undefined;
|
|
174
|
+
|
|
175
|
+
if (operation.parameters.body) {
|
|
176
|
+
const body = operation.parameters.body;
|
|
177
|
+
|
|
178
|
+
if (body.contentTypes.length > 1) {
|
|
179
|
+
reportDiagnostic(ctx.program, {
|
|
180
|
+
code: "dynamic-request-content-type",
|
|
181
|
+
target: operation.operation,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const contentType = body.contentTypes[0] ?? DEFAULT_CONTENT_TYPE;
|
|
186
|
+
|
|
187
|
+
const defaultBodyTypeName = operationNameCase.pascalCase + "RequestBody";
|
|
188
|
+
|
|
189
|
+
const bodyNameCase = parseCase(body.property?.name ?? defaultBodyTypeName);
|
|
190
|
+
|
|
191
|
+
const bodyTypeName = emitTypeReference(
|
|
192
|
+
ctx,
|
|
193
|
+
body.type,
|
|
194
|
+
body.property?.type ?? operation.operation.node,
|
|
195
|
+
module,
|
|
196
|
+
{ altName: defaultBodyTypeName },
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
bodyName = ctx.gensym(bodyNameCase.camelCase);
|
|
200
|
+
|
|
201
|
+
module.imports.push({ binder: ["parseHeaderValueParameters"], from: headerHelpers });
|
|
202
|
+
|
|
203
|
+
const contentTypeHeader = ctx.gensym("contentType");
|
|
204
|
+
|
|
205
|
+
yield ` const ${contentTypeHeader} = parseHeaderValueParameters(${names.ctx}.request.headers["content-type"] as string | undefined);`;
|
|
206
|
+
|
|
207
|
+
yield ` if (${contentTypeHeader}?.value !== ${JSON.stringify(contentType)}) {`;
|
|
208
|
+
|
|
209
|
+
yield ` return ${names.ctx}.errorHandlers.onInvalidRequest(`;
|
|
210
|
+
yield ` ${names.ctx},`;
|
|
211
|
+
yield ` ${JSON.stringify(operation.path)},`;
|
|
212
|
+
yield ` \`unexpected "content-type": '\${${contentTypeHeader}?.value}', expected '${JSON.stringify(contentType)}'\``;
|
|
213
|
+
yield ` );`;
|
|
214
|
+
|
|
215
|
+
yield " }";
|
|
216
|
+
yield "";
|
|
217
|
+
|
|
218
|
+
switch (contentType) {
|
|
219
|
+
case "application/merge-patch+json":
|
|
220
|
+
case "application/json": {
|
|
221
|
+
requireSerialization(ctx, body.type as SerializableType, "application/json");
|
|
222
|
+
yield ` const ${bodyName} = await new Promise(function parse${bodyNameCase.pascalCase}(resolve, reject) {`;
|
|
223
|
+
yield ` const chunks: Array<Buffer> = [];`;
|
|
224
|
+
yield ` ${names.ctx}.request.on("data", function appendChunk(chunk) { chunks.push(chunk); });`;
|
|
225
|
+
yield ` ${names.ctx}.request.on("end", function finalize() {`;
|
|
226
|
+
yield ` try {`;
|
|
227
|
+
yield ` const body = Buffer.concat(chunks).toString();`;
|
|
228
|
+
|
|
229
|
+
let value: string;
|
|
230
|
+
|
|
231
|
+
if (requiresJsonSerialization(ctx, body.type)) {
|
|
232
|
+
value = `${bodyTypeName}.fromJsonObject(JSON.parse(body))`;
|
|
233
|
+
} else {
|
|
234
|
+
value = `JSON.parse(body)`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
yield ` resolve(${value});`;
|
|
238
|
+
yield ` } catch {`;
|
|
239
|
+
yield ` ${names.ctx}.errorHandlers.onInvalidRequest(`;
|
|
240
|
+
yield ` ${names.ctx},`;
|
|
241
|
+
yield ` ${JSON.stringify(operation.path)},`;
|
|
242
|
+
yield ` "invalid JSON in request body",`;
|
|
243
|
+
yield ` );`;
|
|
244
|
+
yield ` reject();`;
|
|
245
|
+
yield ` }`;
|
|
246
|
+
yield ` });`;
|
|
247
|
+
yield ` ${names.ctx}.request.on("error", reject);`;
|
|
248
|
+
yield ` }) as ${bodyTypeName};`;
|
|
249
|
+
yield "";
|
|
250
|
+
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
case "multipart/form-data":
|
|
254
|
+
if (body.bodyKind === "multipart") {
|
|
255
|
+
yield* indent(
|
|
256
|
+
emitMultipart(ctx, module, operation, body, names.ctx, bodyName, bodyTypeName),
|
|
257
|
+
);
|
|
258
|
+
} else {
|
|
259
|
+
yield* indent(emitMultipartLegacy(names.ctx, bodyName, bodyTypeName));
|
|
260
|
+
}
|
|
261
|
+
break;
|
|
262
|
+
default:
|
|
263
|
+
throw new UnimplementedError(`request deserialization for content-type: '${contentType}'`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
yield "";
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let hasOptions = false;
|
|
270
|
+
const optionalParams = new Map<string, string>();
|
|
271
|
+
|
|
272
|
+
const requiredParams = [];
|
|
273
|
+
|
|
274
|
+
for (const param of parameters) {
|
|
275
|
+
let paramBaseExpression;
|
|
276
|
+
const paramNameCase = parseCase(param.name);
|
|
277
|
+
const isBodyField = bodyFields.has(param.name) && bodyFields.get(param.name) === param.type;
|
|
278
|
+
const isBodyExact = operation.parameters.body?.property === param;
|
|
279
|
+
if (isBodyField) {
|
|
280
|
+
paramBaseExpression = `${bodyName}.${paramNameCase.camelCase}`;
|
|
281
|
+
} else if (isBodyExact) {
|
|
282
|
+
paramBaseExpression = bodyName!;
|
|
283
|
+
} else {
|
|
284
|
+
const resolvedParameter = param.type.kind === "ModelProperty" ? param.type : param;
|
|
285
|
+
|
|
286
|
+
paramBaseExpression =
|
|
287
|
+
resolvedParameter.type.kind === "Scalar" && parsedParams.has(resolvedParameter)
|
|
288
|
+
? parseTemplateForScalar(ctx, resolvedParameter.type).replace(
|
|
289
|
+
"{}",
|
|
290
|
+
paramNameCase.camelCase,
|
|
291
|
+
)
|
|
292
|
+
: paramNameCase.camelCase;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (param.optional) {
|
|
296
|
+
hasOptions = true;
|
|
297
|
+
optionalParams.set(paramNameCase.camelCase, paramBaseExpression);
|
|
298
|
+
} else {
|
|
299
|
+
requiredParams.push(paramBaseExpression);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const paramLines = requiredParams.map((p) => `${p},`);
|
|
304
|
+
|
|
305
|
+
if (hasOptions) {
|
|
306
|
+
paramLines.push(
|
|
307
|
+
`{ ${[...optionalParams.entries()].map(([name, expr]) => (name === expr ? name : `${name}: ${expr}`)).join(", ")} }`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const returnType = emitTypeReference(ctx, op.returnType, NoTarget, module, {
|
|
312
|
+
altName: operationNameCase.pascalCase + "Result",
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
yield ` let ${names.result}: ${returnType};`;
|
|
316
|
+
yield "";
|
|
317
|
+
yield ` try {`;
|
|
318
|
+
yield ` ${names.result} = await ${names.operations}.${operationNameCase.camelCase}(${names.ctx}, `;
|
|
319
|
+
yield* indent(indent(indent(paramLines)));
|
|
320
|
+
yield ` );`;
|
|
321
|
+
yield " } catch(e) {";
|
|
322
|
+
yield ` if (${names.isHttpResponder}(e)) {`;
|
|
323
|
+
yield ` return e[${names.httpResponderSym}](${names.ctx});`;
|
|
324
|
+
yield ` } else throw e;`;
|
|
325
|
+
yield ` }`;
|
|
326
|
+
yield "";
|
|
327
|
+
|
|
328
|
+
yield* indent(emitResultProcessing(ctx, names, op.returnType, module));
|
|
329
|
+
|
|
330
|
+
yield "}";
|
|
331
|
+
|
|
332
|
+
yield "";
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
interface Names {
|
|
336
|
+
ctx: string;
|
|
337
|
+
result: string;
|
|
338
|
+
operations: string;
|
|
339
|
+
queryParams: string;
|
|
340
|
+
isHttpResponder: string;
|
|
341
|
+
httpResponderSym: string;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Emit the result-processing code for an operation.
|
|
346
|
+
*
|
|
347
|
+
* This code handles writing the result of calling the business logic layer to the HTTP response object.
|
|
348
|
+
*
|
|
349
|
+
* @param ctx - The HTTP emitter context.
|
|
350
|
+
* @param t - The return type of the operation.
|
|
351
|
+
* @param module - The module that the result processing code will be written to.
|
|
352
|
+
*/
|
|
353
|
+
function* emitResultProcessing(
|
|
354
|
+
ctx: HttpContext,
|
|
355
|
+
names: Names,
|
|
356
|
+
t: Type,
|
|
357
|
+
module: Module,
|
|
358
|
+
): Iterable<string> {
|
|
359
|
+
if (t.kind !== "Union") {
|
|
360
|
+
// Single target type
|
|
361
|
+
yield* emitResultProcessingForType(ctx, names, t, module);
|
|
362
|
+
} else {
|
|
363
|
+
const codeTree = differentiateUnion(ctx, t);
|
|
364
|
+
|
|
365
|
+
yield* writeCodeTree(ctx, codeTree, {
|
|
366
|
+
subject: names.result,
|
|
367
|
+
referenceModelProperty(p) {
|
|
368
|
+
return names.result + "." + parseCase(p.name).camelCase;
|
|
369
|
+
},
|
|
370
|
+
// We mapped the output directly in the code tree input, so we can just return it.
|
|
371
|
+
renderResult: (t) => emitResultProcessingForType(ctx, names, t, module),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Emit the result-processing code for a single response type.
|
|
378
|
+
*
|
|
379
|
+
* @param ctx - The HTTP emitter context.
|
|
380
|
+
* @param target - The target type to emit processing code for.
|
|
381
|
+
* @param module - The module that the result processing code will be written to.
|
|
382
|
+
*/
|
|
383
|
+
function* emitResultProcessingForType(
|
|
384
|
+
ctx: HttpContext,
|
|
385
|
+
names: Names,
|
|
386
|
+
target: Type,
|
|
387
|
+
module: Module,
|
|
388
|
+
): Iterable<string> {
|
|
389
|
+
if (target.kind === "Intrinsic") {
|
|
390
|
+
switch (target.name) {
|
|
391
|
+
case "void":
|
|
392
|
+
yield `${names.ctx}.response.statusCode = 204;`;
|
|
393
|
+
yield `${names.ctx}.response.end();`;
|
|
394
|
+
return;
|
|
395
|
+
case "null":
|
|
396
|
+
yield `${names.ctx}.response.statusCode = 200;`;
|
|
397
|
+
yield `${names.ctx}.response.setHeader("content-type", "application/json");`;
|
|
398
|
+
yield `${names.ctx}.response.end("null");`;
|
|
399
|
+
return;
|
|
400
|
+
case "unknown":
|
|
401
|
+
yield `${names.ctx}.response.statusCode = 200;`;
|
|
402
|
+
yield `${names.ctx}.response.setHeader("content-type", "application/json");`;
|
|
403
|
+
yield `${names.ctx}.response.end(JSON.stringify(${names.result}));`;
|
|
404
|
+
return;
|
|
405
|
+
case "never":
|
|
406
|
+
yield `return ${names.ctx}.errorHandlers.onInternalError(${names.ctx}, "Internal server error.");`;
|
|
407
|
+
return;
|
|
408
|
+
default:
|
|
409
|
+
throw new UnimplementedError(`result processing for intrinsic type '${target.name}'`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (target.kind !== "Model") {
|
|
414
|
+
throw new UnimplementedError(`result processing for type kind '${target.kind}'`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const body = [...target.properties.values()].find((p) => isBody(ctx.program, p));
|
|
418
|
+
|
|
419
|
+
for (const property of target.properties.values()) {
|
|
420
|
+
if (isHeader(ctx.program, property)) {
|
|
421
|
+
const headerName = getHeaderFieldName(ctx.program, property);
|
|
422
|
+
yield `${names.ctx}.response.setHeader(${JSON.stringify(headerName.toLowerCase())}, ${names.result}.${parseCase(property.name).camelCase});`;
|
|
423
|
+
if (!body) yield `delete (${names.result} as any).${parseCase(property.name).camelCase};`;
|
|
424
|
+
} else if (isStatusCode(ctx.program, property)) {
|
|
425
|
+
if (isUnspeakable(property.name)) {
|
|
426
|
+
if (!isValueLiteralType(property.type)) {
|
|
427
|
+
reportDiagnostic(ctx.program, {
|
|
428
|
+
code: "unspeakable-status-code",
|
|
429
|
+
target: property,
|
|
430
|
+
format: {
|
|
431
|
+
name: property.name,
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
compilerAssert(property.type.kind === "Number", "Status code must be a number.");
|
|
438
|
+
|
|
439
|
+
yield `${names.ctx}.response.statusCode = ${property.type.valueAsString};`;
|
|
440
|
+
} else {
|
|
441
|
+
yield `${names.ctx}.response.statusCode = ${names.result}.${parseCase(property.name).camelCase};`;
|
|
442
|
+
if (!body) yield `delete (${names.result} as any).${parseCase(property.name).camelCase};`;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const allMetadataIsRemoved =
|
|
448
|
+
!body &&
|
|
449
|
+
[...target.properties.values()].every((p) => {
|
|
450
|
+
return isHeader(ctx.program, p) || isStatusCode(ctx.program, p);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
if (body) {
|
|
454
|
+
const bodyCase = parseCase(body.name);
|
|
455
|
+
const serializationRequired = isSerializationRequired(ctx, body.type, "application/json");
|
|
456
|
+
requireSerialization(ctx, body.type, "application/json");
|
|
457
|
+
|
|
458
|
+
yield `${names.ctx}.response.setHeader("content-type", "application/json");`;
|
|
459
|
+
|
|
460
|
+
if (serializationRequired) {
|
|
461
|
+
const typeReference = emitTypeReference(ctx, body.type, body, module, {
|
|
462
|
+
requireDeclaration: true,
|
|
463
|
+
});
|
|
464
|
+
yield `${names.ctx}.response.end(JSON.stringify(${typeReference}.toJsonObject(${names.result}.${bodyCase.camelCase})))`;
|
|
465
|
+
} else {
|
|
466
|
+
yield `${names.ctx}.response.end(JSON.stringify(${names.result}.${bodyCase.camelCase}));`;
|
|
467
|
+
}
|
|
468
|
+
} else {
|
|
469
|
+
if (allMetadataIsRemoved) {
|
|
470
|
+
yield `${names.ctx}.response.end();`;
|
|
471
|
+
} else {
|
|
472
|
+
const serializationRequired = isSerializationRequired(ctx, target, "application/json");
|
|
473
|
+
requireSerialization(ctx, target, "application/json");
|
|
474
|
+
|
|
475
|
+
yield `${names.ctx}.response.setHeader("content-type", "application/json");`;
|
|
476
|
+
|
|
477
|
+
if (serializationRequired) {
|
|
478
|
+
const typeReference = emitTypeReference(ctx, target, target, module, {
|
|
479
|
+
requireDeclaration: true,
|
|
480
|
+
});
|
|
481
|
+
yield `${names.ctx}.response.end(JSON.stringify(${typeReference}.toJsonObject(${names.result} as ${typeReference})));`;
|
|
482
|
+
} else {
|
|
483
|
+
yield `${names.ctx}.response.end(JSON.stringify(${names.result}));`;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Emit code that binds a given header parameter to a variable.
|
|
491
|
+
*
|
|
492
|
+
* If the parameter is not optional, this will also emit a test to ensure that the parameter is present.
|
|
493
|
+
*
|
|
494
|
+
* @param ctx - The HTTP emitter context.
|
|
495
|
+
* @param parameter - The header parameter to bind.
|
|
496
|
+
*/
|
|
497
|
+
function* emitHeaderParamBinding(
|
|
498
|
+
ctx: HttpContext,
|
|
499
|
+
operation: HttpOperation,
|
|
500
|
+
names: Names,
|
|
501
|
+
parameter: Extract<HttpOperationParameter, { type: "header" }>,
|
|
502
|
+
): Iterable<string> {
|
|
503
|
+
const nameCase = parseCase(parameter.param.name);
|
|
504
|
+
const headerName = parameter.name.toLowerCase();
|
|
505
|
+
|
|
506
|
+
// See https://nodejs.org/api/http.html#messageheaders
|
|
507
|
+
// Apparently, only set-cookie can be an array.
|
|
508
|
+
const canBeArrayType = parameter.name === "set-cookie";
|
|
509
|
+
|
|
510
|
+
const assertion = canBeArrayType ? "" : " as string | undefined";
|
|
511
|
+
|
|
512
|
+
yield `const ${nameCase.camelCase} = ${names.ctx}.request.headers[${JSON.stringify(headerName)}]${assertion};`;
|
|
513
|
+
|
|
514
|
+
if (!parameter.param.optional) {
|
|
515
|
+
yield `if (${nameCase.camelCase} === undefined) {`;
|
|
516
|
+
// prettier-ignore
|
|
517
|
+
yield ` return ${names.ctx}.errorHandlers.onInvalidRequest(${names.ctx}, ${JSON.stringify(operation.path)}, "missing required header '${headerName}'");`;
|
|
518
|
+
yield "}";
|
|
519
|
+
yield "";
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Emit code that binds a given query parameter to a variable.
|
|
525
|
+
*
|
|
526
|
+
* If the parameter is not optional, this will also emit a test to ensure that the parameter is present.
|
|
527
|
+
*
|
|
528
|
+
* @param ctx - The HTTP emitter context
|
|
529
|
+
* @param parameter - The query parameter to bind
|
|
530
|
+
*/
|
|
531
|
+
function* emitQueryParamBinding(
|
|
532
|
+
ctx: HttpContext,
|
|
533
|
+
operation: HttpOperation,
|
|
534
|
+
names: Names,
|
|
535
|
+
parameter: Extract<HttpOperationParameter, { type: "query" }>,
|
|
536
|
+
): Iterable<string> {
|
|
537
|
+
const nameCase = parseCase(parameter.param.name);
|
|
538
|
+
|
|
539
|
+
// UrlSearchParams annoyingly returns null for missing parameters instead of undefined.
|
|
540
|
+
yield `const ${nameCase.camelCase} = ${names.queryParams}.get(${JSON.stringify(parameter.name)}) ?? undefined;`;
|
|
541
|
+
|
|
542
|
+
if (!parameter.param.optional) {
|
|
543
|
+
yield `if (!${nameCase.camelCase}) {`;
|
|
544
|
+
yield ` return ${names.ctx}.errorHandlers.onInvalidRequest(${names.ctx}, ${JSON.stringify(operation.path)}, "missing required query parameter '${parameter.name}'");`;
|
|
545
|
+
yield "}";
|
|
546
|
+
yield "";
|
|
547
|
+
}
|
|
548
|
+
}
|