@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.
Files changed (215) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/LICENSE +21 -0
  3. package/README.md +183 -0
  4. package/build-helpers.ts +170 -0
  5. package/dist/generated-defs/helpers/header.d.ts +4 -0
  6. package/dist/generated-defs/helpers/header.d.ts.map +1 -0
  7. package/dist/generated-defs/helpers/header.js +76 -0
  8. package/dist/generated-defs/helpers/header.js.map +1 -0
  9. package/dist/generated-defs/helpers/http.d.ts +4 -0
  10. package/dist/generated-defs/helpers/http.d.ts.map +1 -0
  11. package/dist/generated-defs/helpers/http.js +134 -0
  12. package/dist/generated-defs/helpers/http.js.map +1 -0
  13. package/dist/generated-defs/helpers/index.d.ts +4 -0
  14. package/dist/generated-defs/helpers/index.d.ts.map +1 -0
  15. package/dist/generated-defs/helpers/index.js +21 -0
  16. package/dist/generated-defs/helpers/index.js.map +1 -0
  17. package/dist/generated-defs/helpers/multipart.d.ts +4 -0
  18. package/dist/generated-defs/helpers/multipart.d.ts.map +1 -0
  19. package/dist/generated-defs/helpers/multipart.js +249 -0
  20. package/dist/generated-defs/helpers/multipart.js.map +1 -0
  21. package/dist/generated-defs/helpers/router.d.ts +4 -0
  22. package/dist/generated-defs/helpers/router.d.ts.map +1 -0
  23. package/dist/generated-defs/helpers/router.js +259 -0
  24. package/dist/generated-defs/helpers/router.js.map +1 -0
  25. package/dist/src/common/declaration.d.ts +13 -0
  26. package/dist/src/common/declaration.d.ts.map +1 -0
  27. package/dist/src/common/declaration.js +45 -0
  28. package/dist/src/common/declaration.js.map +1 -0
  29. package/dist/src/common/documentation.d.ts +12 -0
  30. package/dist/src/common/documentation.d.ts.map +1 -0
  31. package/dist/src/common/documentation.js +21 -0
  32. package/dist/src/common/documentation.js.map +1 -0
  33. package/dist/src/common/enum.d.ts +10 -0
  34. package/dist/src/common/enum.d.ts.map +1 -0
  35. package/dist/src/common/enum.js +21 -0
  36. package/dist/src/common/enum.js.map +1 -0
  37. package/dist/src/common/interface.d.ts +50 -0
  38. package/dist/src/common/interface.d.ts.map +1 -0
  39. package/dist/src/common/interface.js +194 -0
  40. package/dist/src/common/interface.js.map +1 -0
  41. package/dist/src/common/model.d.ts +26 -0
  42. package/dist/src/common/model.d.ts.map +1 -0
  43. package/dist/src/common/model.js +115 -0
  44. package/dist/src/common/model.js.map +1 -0
  45. package/dist/src/common/namespace.d.ts +38 -0
  46. package/dist/src/common/namespace.d.ts.map +1 -0
  47. package/dist/src/common/namespace.js +184 -0
  48. package/dist/src/common/namespace.js.map +1 -0
  49. package/dist/src/common/reference.d.ts +46 -0
  50. package/dist/src/common/reference.d.ts.map +1 -0
  51. package/dist/src/common/reference.js +243 -0
  52. package/dist/src/common/reference.js.map +1 -0
  53. package/dist/src/common/scalar.d.ts +50 -0
  54. package/dist/src/common/scalar.d.ts.map +1 -0
  55. package/dist/src/common/scalar.js +144 -0
  56. package/dist/src/common/scalar.js.map +1 -0
  57. package/dist/src/common/serialization/index.d.ts +11 -0
  58. package/dist/src/common/serialization/index.d.ts.map +1 -0
  59. package/dist/src/common/serialization/index.js +72 -0
  60. package/dist/src/common/serialization/index.js.map +1 -0
  61. package/dist/src/common/serialization/json.d.ts +6 -0
  62. package/dist/src/common/serialization/json.d.ts.map +1 -0
  63. package/dist/src/common/serialization/json.js +341 -0
  64. package/dist/src/common/serialization/json.js.map +1 -0
  65. package/dist/src/common/union.d.ts +23 -0
  66. package/dist/src/common/union.d.ts.map +1 -0
  67. package/dist/src/common/union.js +57 -0
  68. package/dist/src/common/union.js.map +1 -0
  69. package/dist/src/ctx.d.ts +242 -0
  70. package/dist/src/ctx.d.ts.map +1 -0
  71. package/dist/src/ctx.js +211 -0
  72. package/dist/src/ctx.js.map +1 -0
  73. package/dist/src/helpers/header.d.ts +14 -0
  74. package/dist/src/helpers/header.d.ts.map +1 -0
  75. package/dist/src/helpers/header.js +38 -0
  76. package/dist/src/helpers/header.js.map +1 -0
  77. package/dist/src/helpers/http.d.ts +70 -0
  78. package/dist/src/helpers/http.d.ts.map +1 -0
  79. package/dist/src/helpers/http.js +86 -0
  80. package/dist/src/helpers/http.js.map +1 -0
  81. package/dist/src/helpers/multipart.d.ts +26 -0
  82. package/dist/src/helpers/multipart.d.ts.map +1 -0
  83. package/dist/src/helpers/multipart.js +182 -0
  84. package/dist/src/helpers/multipart.js.map +1 -0
  85. package/dist/src/helpers/router.d.ts +176 -0
  86. package/dist/src/helpers/router.d.ts.map +1 -0
  87. package/dist/src/helpers/router.js +55 -0
  88. package/dist/src/helpers/router.js.map +1 -0
  89. package/dist/src/http/index.d.ts +24 -0
  90. package/dist/src/http/index.d.ts.map +1 -0
  91. package/dist/src/http/index.js +52 -0
  92. package/dist/src/http/index.js.map +1 -0
  93. package/dist/src/http/server/index.d.ts +11 -0
  94. package/dist/src/http/server/index.d.ts.map +1 -0
  95. package/dist/src/http/server/index.js +413 -0
  96. package/dist/src/http/server/index.js.map +1 -0
  97. package/dist/src/http/server/multipart.d.ts +16 -0
  98. package/dist/src/http/server/multipart.d.ts.map +1 -0
  99. package/dist/src/http/server/multipart.js +214 -0
  100. package/dist/src/http/server/multipart.js.map +1 -0
  101. package/dist/src/http/server/router.d.ts +15 -0
  102. package/dist/src/http/server/router.d.ts.map +1 -0
  103. package/dist/src/http/server/router.js +459 -0
  104. package/dist/src/http/server/router.js.map +1 -0
  105. package/dist/src/index.d.ts +5 -0
  106. package/dist/src/index.d.ts.map +1 -0
  107. package/dist/src/index.js +38 -0
  108. package/dist/src/index.js.map +1 -0
  109. package/dist/src/lib.d.ts +141 -0
  110. package/dist/src/lib.d.ts.map +1 -0
  111. package/dist/src/lib.js +116 -0
  112. package/dist/src/lib.js.map +1 -0
  113. package/dist/src/scripts/scaffold/bin.d.mts +14 -0
  114. package/dist/src/scripts/scaffold/bin.d.mts.map +1 -0
  115. package/dist/src/scripts/scaffold/bin.mjs +559 -0
  116. package/dist/src/scripts/scaffold/bin.mjs.map +1 -0
  117. package/dist/src/testing/index.d.ts +3 -0
  118. package/dist/src/testing/index.d.ts.map +1 -0
  119. package/dist/src/testing/index.js +6 -0
  120. package/dist/src/testing/index.js.map +1 -0
  121. package/dist/src/util/case.d.ts +81 -0
  122. package/dist/src/util/case.d.ts.map +1 -0
  123. package/dist/src/util/case.js +111 -0
  124. package/dist/src/util/case.js.map +1 -0
  125. package/dist/src/util/differentiate.d.ts +251 -0
  126. package/dist/src/util/differentiate.d.ts.map +1 -0
  127. package/dist/src/util/differentiate.js +580 -0
  128. package/dist/src/util/differentiate.js.map +1 -0
  129. package/dist/src/util/error.d.ts +13 -0
  130. package/dist/src/util/error.d.ts.map +1 -0
  131. package/dist/src/util/error.js +25 -0
  132. package/dist/src/util/error.js.map +1 -0
  133. package/dist/src/util/extends.d.ts +10 -0
  134. package/dist/src/util/extends.d.ts.map +1 -0
  135. package/dist/src/util/extends.js +31 -0
  136. package/dist/src/util/extends.js.map +1 -0
  137. package/dist/src/util/iter.d.ts +39 -0
  138. package/dist/src/util/iter.d.ts.map +1 -0
  139. package/dist/src/util/iter.js +72 -0
  140. package/dist/src/util/iter.js.map +1 -0
  141. package/dist/src/util/keywords.d.ts +10 -0
  142. package/dist/src/util/keywords.d.ts.map +1 -0
  143. package/dist/src/util/keywords.js +85 -0
  144. package/dist/src/util/keywords.js.map +1 -0
  145. package/dist/src/util/name.d.ts +12 -0
  146. package/dist/src/util/name.d.ts.map +1 -0
  147. package/dist/src/util/name.js +26 -0
  148. package/dist/src/util/name.js.map +1 -0
  149. package/dist/src/util/once-queue.d.ts +24 -0
  150. package/dist/src/util/once-queue.d.ts.map +1 -0
  151. package/dist/src/util/once-queue.js +34 -0
  152. package/dist/src/util/once-queue.js.map +1 -0
  153. package/dist/src/util/openapi3.d.ts +23 -0
  154. package/dist/src/util/openapi3.d.ts.map +1 -0
  155. package/dist/src/util/openapi3.js +40 -0
  156. package/dist/src/util/openapi3.js.map +1 -0
  157. package/dist/src/util/pluralism.d.ts +23 -0
  158. package/dist/src/util/pluralism.d.ts.map +1 -0
  159. package/dist/src/util/pluralism.js +36 -0
  160. package/dist/src/util/pluralism.js.map +1 -0
  161. package/dist/src/util/scope.d.ts +85 -0
  162. package/dist/src/util/scope.d.ts.map +1 -0
  163. package/dist/src/util/scope.js +111 -0
  164. package/dist/src/util/scope.js.map +1 -0
  165. package/dist/src/write.d.ts +23 -0
  166. package/dist/src/write.d.ts.map +1 -0
  167. package/dist/src/write.js +62 -0
  168. package/dist/src/write.js.map +1 -0
  169. package/generated-defs/helpers/header.ts +83 -0
  170. package/generated-defs/helpers/http.ts +141 -0
  171. package/generated-defs/helpers/index.ts +27 -0
  172. package/generated-defs/helpers/multipart.ts +256 -0
  173. package/generated-defs/helpers/router.ts +266 -0
  174. package/package.json +71 -0
  175. package/src/common/declaration.ts +52 -0
  176. package/src/common/documentation.ts +26 -0
  177. package/src/common/enum.ts +28 -0
  178. package/src/common/interface.ts +264 -0
  179. package/src/common/model.ts +160 -0
  180. package/src/common/namespace.ts +243 -0
  181. package/src/common/reference.ts +319 -0
  182. package/src/common/scalar.ts +173 -0
  183. package/src/common/serialization/index.ts +124 -0
  184. package/src/common/serialization/json.ts +444 -0
  185. package/src/common/union.ts +76 -0
  186. package/src/ctx.ts +497 -0
  187. package/src/helpers/header.ts +55 -0
  188. package/src/helpers/http.ts +113 -0
  189. package/src/helpers/multipart.ts +228 -0
  190. package/src/helpers/router.ts +238 -0
  191. package/src/http/index.ts +81 -0
  192. package/src/http/server/index.ts +548 -0
  193. package/src/http/server/multipart.ts +272 -0
  194. package/src/http/server/router.ts +686 -0
  195. package/src/index.ts +56 -0
  196. package/src/lib.ts +130 -0
  197. package/src/scripts/scaffold/bin.mts +781 -0
  198. package/src/testing/index.ts +10 -0
  199. package/src/util/case.ts +182 -0
  200. package/src/util/differentiate.ts +957 -0
  201. package/src/util/error.ts +28 -0
  202. package/src/util/extends.ts +43 -0
  203. package/src/util/iter.ts +85 -0
  204. package/src/util/keywords.ts +90 -0
  205. package/src/util/name.ts +33 -0
  206. package/src/util/once-queue.ts +55 -0
  207. package/src/util/openapi3.ts +53 -0
  208. package/src/util/pluralism.ts +37 -0
  209. package/src/util/scope.ts +211 -0
  210. package/src/write.ts +88 -0
  211. package/temp/tsconfig.tsbuildinfo +1 -0
  212. package/test/header.test.ts +26 -0
  213. package/test/multipart.test.ts +169 -0
  214. package/tsconfig.json +10 -0
  215. package/vitest.config.ts +4 -0
@@ -0,0 +1,228 @@
1
+ // Copyright (c) Microsoft Corporation
2
+ // Licensed under the MIT license.
3
+
4
+ import type * as http from "node:http";
5
+
6
+ export interface HttpPart {
7
+ headers: { [k: string]: string | undefined };
8
+ body: ReadableStream<Buffer>;
9
+ }
10
+
11
+ /**
12
+ * Consumes a stream of incoming data and splits it into individual streams for each part of a multipart request, using
13
+ * the provided `boundary` value.
14
+ */
15
+ function MultipartBoundaryTransformStream(
16
+ boundary: string,
17
+ ): ReadableWritablePair<ReadableStream<Buffer>, Buffer> {
18
+ let buffer: Buffer = Buffer.alloc(0);
19
+ // Initialize subcontroller to an object that does nothing. Multipart bodies may contain a preamble before the first
20
+ // boundary, so this dummy controller will discard it.
21
+ let subController: { enqueue(chunk: Buffer): void; close(): void } | null = {
22
+ enqueue() {},
23
+ close() {},
24
+ };
25
+
26
+ let boundarySplit = Buffer.from(`--${boundary}`);
27
+ let initialized = false;
28
+
29
+ // We need to keep at least the length of the boundary split plus room for CRLFCRLF in the buffer to detect the boundaries.
30
+ // We subtract one from this length because if the whole thing were in the buffer, we would detect it and move past it.
31
+ const bufferKeepLength = boundarySplit.length + BUF_CRLFCRLF.length - 1;
32
+ let _readableController: ReadableStreamDefaultController<ReadableStream<Buffer>> = null as any;
33
+
34
+ const readable = new ReadableStream<ReadableStream<Buffer>>({
35
+ start(controller) {
36
+ _readableController = controller;
37
+ },
38
+ });
39
+
40
+ const readableController = _readableController;
41
+
42
+ const writable = new WritableStream<Buffer>({
43
+ write: async (chunk) => {
44
+ buffer = Buffer.concat([buffer, chunk]);
45
+
46
+ let index: number;
47
+
48
+ while ((index = buffer.indexOf(boundarySplit)) !== -1) {
49
+ // We found a boundary, emit everything before it and initialize a new stream for the next part.
50
+
51
+ // We are initialized if we have found the boundary at least once.
52
+ //
53
+ // Cases
54
+ // 1. If the index is zero and we aren't initialized, there was no preamble.
55
+ // 2. If the index is zero and we are initialized, then we had to have found \r\n--boundary, nothing special to do.
56
+ // 3. If the index is not zero, and we are initialized, then we found \r\n--boundary somewhere in the middle,
57
+ // nothing special to do.
58
+ // 4. If the index is not zero and we aren't initialized, then we need to check that boundarySplit was preceded
59
+ // by \r\n for validity, because the preamble must end with \r\n.
60
+
61
+ if (index > 0) {
62
+ if (!initialized) {
63
+ if (!buffer.subarray(index - 2, index).equals(Buffer.from("\r\n"))) {
64
+ readableController.error(new Error("Invalid preamble in multipart body."));
65
+ } else {
66
+ await enqueueSub(buffer.subarray(0, index - 2));
67
+ }
68
+ } else {
69
+ await enqueueSub(buffer.subarray(0, index));
70
+ }
71
+ }
72
+
73
+ // We enqueued everything before the boundary, so we clear the buffer past the boundary
74
+ buffer = buffer.subarray(index + boundarySplit.length);
75
+
76
+ // We're done with the current part, so close the stream. If this is the opening boundary, there won't be a
77
+ // subcontroller yet.
78
+ subController?.close();
79
+ subController = null;
80
+
81
+ if (!initialized) {
82
+ initialized = true;
83
+ boundarySplit = Buffer.from(`\r\n${boundarySplit}`);
84
+ }
85
+ }
86
+
87
+ if (buffer.length > bufferKeepLength) {
88
+ await enqueueSub(buffer.subarray(0, -bufferKeepLength));
89
+ buffer = buffer.subarray(-bufferKeepLength);
90
+ }
91
+ },
92
+ close() {
93
+ if (!/--(\r\n)?/.test(buffer.toString("utf-8"))) {
94
+ readableController.error(new Error("Unexpected characters after final boundary."));
95
+ }
96
+
97
+ subController?.close();
98
+
99
+ readableController.close();
100
+ },
101
+ });
102
+
103
+ async function enqueueSub(s: Buffer) {
104
+ subController ??= await new Promise<ReadableStreamDefaultController>((resolve) => {
105
+ readableController.enqueue(
106
+ new ReadableStream<Buffer>({
107
+ start: (controller) => resolve(controller),
108
+ }),
109
+ );
110
+ });
111
+
112
+ subController.enqueue(s);
113
+ }
114
+
115
+ return { readable, writable };
116
+ }
117
+
118
+ const BUF_CRLFCRLF = Buffer.from("\r\n\r\n");
119
+
120
+ /**
121
+ * Consumes a stream of the contents of a single part of a multipart request and emits an `HttpPart` object for each part.
122
+ * This consumes just enough of the stream to read the headers, and then forwards the rest of the stream as the body.
123
+ */
124
+ class HttpPartTransform extends TransformStream<ReadableStream<Buffer>, HttpPart> {
125
+ constructor() {
126
+ super({
127
+ transform: async (partRaw, controller) => {
128
+ const reader = partRaw.getReader();
129
+
130
+ let buf = Buffer.alloc(0);
131
+ let idx;
132
+
133
+ while ((idx = buf.indexOf(BUF_CRLFCRLF)) === -1) {
134
+ const { done, value } = await reader.read();
135
+ if (done) {
136
+ throw new Error("Unexpected end of part.");
137
+ }
138
+ buf = Buffer.concat([buf, value]);
139
+ }
140
+
141
+ const headerText = buf.subarray(0, idx).toString("utf-8").trim();
142
+
143
+ const headers = Object.fromEntries(
144
+ headerText.split("\r\n").map((line) => {
145
+ const [name, value] = line.split(": ", 2);
146
+
147
+ return [name.toLowerCase(), value];
148
+ }),
149
+ ) as { [k: string]: string };
150
+
151
+ const body = new ReadableStream<Buffer>({
152
+ start(controller) {
153
+ controller.enqueue(buf.subarray(idx + BUF_CRLFCRLF.length));
154
+ },
155
+ async pull(controller) {
156
+ const { done, value } = await reader.read();
157
+
158
+ if (done) {
159
+ controller.close();
160
+ } else {
161
+ controller.enqueue(value);
162
+ }
163
+ },
164
+ });
165
+
166
+ controller.enqueue({ headers, body });
167
+ },
168
+ });
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Processes a request as a multipart request, returning a stream of `HttpPart` objects, each representing an individual
174
+ * part in the multipart request.
175
+ *
176
+ * Only call this function if you have already validated the content type of the request and confirmed that it is a
177
+ * multipart request.
178
+ *
179
+ * @throws Error if the content-type header is missing or does not contain a boundary field.
180
+ *
181
+ * @param request - the incoming request to parse as multipart
182
+ * @returns a stream of HttpPart objects, each representing an individual part in the multipart request
183
+ */
184
+ export function createMultipartReadable(request: http.IncomingMessage): ReadableStream<HttpPart> {
185
+ const boundary = request.headers["content-type"]
186
+ ?.split(";")
187
+ .find((s) => s.includes("boundary="))
188
+ ?.split("=", 2)[1];
189
+ if (!boundary) {
190
+ throw new Error("Invalid request: missing boundary in content-type.");
191
+ }
192
+
193
+ const bodyStream = new ReadableStream<Uint8Array>({
194
+ start(controller) {
195
+ request.on("data", (chunk: Buffer) => {
196
+ controller.enqueue(chunk);
197
+ });
198
+ request.on("end", () => controller.close());
199
+ },
200
+ });
201
+
202
+ return bodyStream
203
+ .pipeThrough(MultipartBoundaryTransformStream(boundary))
204
+ .pipeThrough(new HttpPartTransform());
205
+ }
206
+
207
+ // Gross polyfill because Safari doesn't support this yet.
208
+ //
209
+ // https://bugs.webkit.org/show_bug.cgi?id=194379
210
+ // https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream#browser_compatibility
211
+ (ReadableStream.prototype as any)[Symbol.asyncIterator] ??= async function* () {
212
+ const reader = this.getReader();
213
+ try {
214
+ while (true) {
215
+ const { done, value } = await reader.read();
216
+ if (done) return value;
217
+ yield value;
218
+ }
219
+ } finally {
220
+ reader.releaseLock();
221
+ }
222
+ };
223
+
224
+ declare global {
225
+ interface ReadableStream<R> {
226
+ [Symbol.asyncIterator](): AsyncIterableIterator<R>;
227
+ }
228
+ }
@@ -0,0 +1,238 @@
1
+ // Copyright (c) Microsoft Corporation
2
+ // Licensed under the MIT license.
3
+
4
+ import type * as http from "node:http";
5
+
6
+ /** A policy that can be applied to a route or a set of routes. */
7
+ export interface Policy {
8
+ /** Optional policy name. */
9
+ name?: string;
10
+
11
+ /**
12
+ * Applies the policy to the request.
13
+ *
14
+ * Policies _MUST_ call `next()` to pass the request to the next policy _OR_ call `response.end()` to terminate,
15
+ * and _MUST NOT_ do both.
16
+ *
17
+ * If the policy passes a `request` object to `next()`, that request object will be used instead of the original
18
+ * request object for the remainder of the policy chain. If the policy does _not_ pass a request object to `next()`,
19
+ * the same object that was passed to this policy will be forwarded to the next policy automatically.
20
+ *
21
+ * @param request - The incoming HTTP request.
22
+ * @param response - The outgoing HTTP response.
23
+ * @param next - Calls the next policy in the chain.
24
+ */
25
+ (ctx: HttpContext, next: (request?: http.IncomingMessage) => void): void;
26
+ }
27
+
28
+ /**
29
+ * Create a function from a chain of policies.
30
+ *
31
+ * This returns a single function that will apply the policy chain and eventually call the provided `next()` function.
32
+ *
33
+ * @param name - The name to give to the policy chain function.
34
+ * @param policies - The policies to apply to the request.
35
+ * @param out - The function to call after the policies have been applied.
36
+ */
37
+ export function createPolicyChain<Out extends (ctx: HttpContext, ...rest: any[]) => void>(
38
+ name: string,
39
+ policies: Policy[],
40
+ out: Out,
41
+ ): Out {
42
+ let outParams: any[];
43
+ if (policies.length === 0) {
44
+ return out;
45
+ }
46
+
47
+ function applyPolicy(ctx: HttpContext, index: number) {
48
+ if (index >= policies.length) {
49
+ return out(ctx, ...outParams);
50
+ }
51
+
52
+ policies[index](ctx, function nextPolicy(nextRequest) {
53
+ applyPolicy(
54
+ {
55
+ ...ctx,
56
+ request: nextRequest ?? ctx.request,
57
+ },
58
+ index + 1,
59
+ );
60
+ });
61
+ }
62
+
63
+ return {
64
+ [name](ctx: HttpContext, ...params: any[]) {
65
+ outParams = params;
66
+ applyPolicy(ctx, 0);
67
+ },
68
+ }[name] as Out;
69
+ }
70
+
71
+ /**
72
+ * The type of an error encountered during request validation.
73
+ */
74
+ export type ValidationError = string;
75
+
76
+ /**
77
+ * An object specifying the policies for a given route configuration.
78
+ */
79
+ export type RoutePolicies<RouteConfig extends { [k: string]: object }> = {
80
+ [Interface in keyof RouteConfig]?: {
81
+ before?: Policy[];
82
+ after?: Policy[];
83
+ methodPolicies?: {
84
+ [Method in keyof RouteConfig[Interface]]?: Policy[];
85
+ };
86
+ };
87
+ };
88
+
89
+ /**
90
+ * Create a policy chain for a given route.
91
+ *
92
+ * This function calls `createPolicyChain` internally and orders the policies based on the route configuration.
93
+ *
94
+ * Interface-level `before` policies run first, then method-level policies, then Interface-level `after` policies.
95
+ *
96
+ * @param name - The name to give to the policy chain function.
97
+ * @param routePolicies - The policies to apply to the routes (part of the route configuration).
98
+ * @param interfaceName - The name of the interface that the route belongs to.
99
+ * @param methodName - The name of the method that the route corresponds to.
100
+ * @param out - The function to call after the policies have been applied.
101
+ */
102
+ export function createPolicyChainForRoute<
103
+ RouteConfig extends { [k: string]: object },
104
+ InterfaceName extends keyof RouteConfig,
105
+ Out extends (ctx: HttpContext, ...rest: any[]) => void,
106
+ >(
107
+ name: string,
108
+ routePolicies: RoutePolicies<RouteConfig>,
109
+ interfaceName: InterfaceName,
110
+ methodName: keyof RouteConfig[InterfaceName],
111
+ out: Out,
112
+ ): Out {
113
+ return createPolicyChain(
114
+ name,
115
+ [
116
+ ...(routePolicies[interfaceName]?.before ?? []),
117
+ ...(routePolicies[interfaceName]?.methodPolicies?.[methodName] ?? []),
118
+ ...(routePolicies[interfaceName]?.after ?? []),
119
+ ],
120
+ out,
121
+ );
122
+ }
123
+
124
+ /**
125
+ * Options for configuring a router with additional functionality.
126
+ */
127
+ export interface RouterOptions<
128
+ RouteConfig extends { [k: string]: object } = { [k: string]: object },
129
+ > {
130
+ /**
131
+ * The base path of the router.
132
+ *
133
+ * This should include any leading slashes, but not a trailing slash, and should not include any component
134
+ * of the URL authority (e.g. the scheme, host, or port).
135
+ *
136
+ * Defaults to "".
137
+ */
138
+ basePath?: string;
139
+
140
+ /**
141
+ * A list of policies to apply to all routes _before_ routing.
142
+ *
143
+ * Policies are applied in the order they are listed.
144
+ *
145
+ * By default, the policy list is empty.
146
+ *
147
+ * Policies _MUST_ call `next()` to pass the request to the next policy _OR_ call `response.end()` to terminate
148
+ * the response and _MUST NOT_ do both.
149
+ */
150
+ policies?: Policy[];
151
+
152
+ /**
153
+ * A record of policies that apply to specific routes.
154
+ *
155
+ * The policies are provided as a nested record where the keys are the business-logic interface names, and the values
156
+ * are records of the method names in the given interface and the policies that apply to them.
157
+ *
158
+ * By default, no additional policies are applied to the routes.
159
+ *
160
+ * Policies _MUST_ call `next()` to pass the request to the next policy _OR_ call `response.end()` to terminate
161
+ * the response and _MUST NOT_ do both.
162
+ */
163
+ routePolicies?: RoutePolicies<RouteConfig>;
164
+
165
+ /**
166
+ * A handler for requests where the resource is not found.
167
+ *
168
+ * The router will call this function when no route matches the incoming request.
169
+ *
170
+ * If this handler is not provided, a 404 Not Found response with a text body will be returned.
171
+ *
172
+ * You _MUST_ call `response.end()` to terminate the response.
173
+ *
174
+ * This handler is unreachable when using the Express middleware, as it will forward non-matching requests to the
175
+ * next middleware layer in the stack.
176
+ *
177
+ * @param ctx - The HTTP context for the request.
178
+ */
179
+ onRequestNotFound?: (ctx: HttpContext) => void;
180
+
181
+ /**
182
+ * A handler for requests that fail to validate inputs.
183
+ *
184
+ * If this handler is not provided, a 400 Bad Request response with a JSON body containing some basic information
185
+ * about the error will be returned to the client.
186
+ *
187
+ * You _MUST_ call `response.end()` to terminate the response.
188
+ *
189
+ * @param ctx - The HTTP context for the request.
190
+ * @param route - The route that was matched.
191
+ * @param error - The validation error that was thrown.
192
+ */
193
+ onInvalidRequest?: (ctx: HttpContext, route: string, error: ValidationError) => void;
194
+
195
+ /**
196
+ * A handler for requests that throw an error during processing.
197
+ *
198
+ * If this handler is not provided, a 500 Internal Server Error response with a text body and no error details will be
199
+ * returned to the client.
200
+ *
201
+ * You _MUST_ call `response.end()` to terminate the response.
202
+ *
203
+ * If this handler itself throws an Error, the router will respond with a 500 Internal Server Error
204
+ *
205
+ * @param ctx - The HTTP context for the request.
206
+ * @param error - The error that was thrown.
207
+ */
208
+ onInternalError?(ctx: HttpContext, error: Error): void;
209
+ }
210
+
211
+ /** Context information for operations carried over the HTTP protocol. */
212
+ export interface HttpContext {
213
+ /** The incoming request to the server. */
214
+ request: http.IncomingMessage;
215
+ /** The outgoing response object. */
216
+ response: http.ServerResponse;
217
+
218
+ /**
219
+ * Error handling functions provided by the HTTP router. Service implementations may call these methods in case a
220
+ * resource is not found, a request is invalid, or an internal error occurs.
221
+ *
222
+ * These methods will respond to the client with the appropriate status code and message.
223
+ */
224
+ errorHandlers: {
225
+ /**
226
+ * Signals that the requested resource was not found.
227
+ */
228
+ onRequestNotFound: Exclude<RouterOptions["onRequestNotFound"], undefined>;
229
+ /**
230
+ * Signals that the request was invalid.
231
+ */
232
+ onInvalidRequest: Exclude<RouterOptions["onInvalidRequest"], undefined>;
233
+ /**
234
+ * Signals that an internal error occurred.
235
+ */
236
+ onInternalError: Exclude<RouterOptions["onInternalError"], undefined>;
237
+ };
238
+ }
@@ -0,0 +1,81 @@
1
+ // Copyright (c) Microsoft Corporation
2
+ // Licensed under the MIT license.
3
+
4
+ import { NoTarget } from "@typespec/compiler";
5
+ import { HttpServer, HttpService, getHttpService, getServers } from "@typespec/http";
6
+ import { JsContext, Module, createModule } from "../ctx.js";
7
+ import { reportDiagnostic } from "../lib.js";
8
+ import { getOpenApi3Emitter, getOpenApi3ServiceRecord, tryGetOpenApi3 } from "../util/openapi3.js";
9
+ import { emitRawServer } from "./server/index.js";
10
+ import { emitRouter } from "./server/router.js";
11
+
12
+ /**
13
+ * Additional context items used by the HTTP emitter.
14
+ */
15
+ export interface HttpContext extends JsContext {
16
+ /**
17
+ * The HTTP-level representation of the service.
18
+ */
19
+ httpService: HttpService;
20
+ /**
21
+ * The root module for HTTP-specific code.
22
+ */
23
+ httpModule: Module;
24
+ /**
25
+ * The server definitions of the service (\@server decorator)
26
+ */
27
+ servers: HttpServer[];
28
+ }
29
+
30
+ /**
31
+ * Emits bindings for the service to be carried over the HTTP protocol.
32
+ */
33
+ export async function emitHttp(ctx: JsContext) {
34
+ const [httpService, diagnostics] = getHttpService(ctx.program, ctx.service.type);
35
+
36
+ const diagnosticsAreError = diagnostics.some((d) => d.severity === "error");
37
+
38
+ if (diagnosticsAreError) {
39
+ reportDiagnostic(ctx.program, {
40
+ code: "http-emit-disabled",
41
+ target: NoTarget,
42
+ messageId: "default",
43
+ });
44
+ return;
45
+ }
46
+
47
+ const servers = getServers(ctx.program, ctx.service.type) ?? [];
48
+
49
+ const httpModule = createModule("http", ctx.generatedModule);
50
+
51
+ const httpContext: HttpContext = {
52
+ ...ctx,
53
+ httpService,
54
+ httpModule,
55
+ servers,
56
+ };
57
+
58
+ const openapi3Emitter = await getOpenApi3Emitter();
59
+ const openapi3 = await tryGetOpenApi3(ctx.program, ctx.service);
60
+
61
+ if (openapi3) {
62
+ const openApiDocumentModule = createModule("openapi3", httpModule);
63
+
64
+ openApiDocumentModule.declarations.push([
65
+ `export const openApiDocument = ${JSON.stringify(openapi3)}`,
66
+ ]);
67
+ } else if (openapi3Emitter) {
68
+ const serviceRecord = await getOpenApi3ServiceRecord(ctx.program, ctx.service);
69
+
70
+ reportDiagnostic(ctx.program, {
71
+ code: "openapi3-document-not-generated",
72
+ target: ctx.service.type,
73
+ messageId: serviceRecord?.versioned ? "versioned" : "unable",
74
+ });
75
+ }
76
+
77
+ const operationsModule = createModule("operations", httpModule);
78
+
79
+ const serverRawModule = emitRawServer(httpContext, operationsModule);
80
+ emitRouter(httpContext, httpService, serverRawModule);
81
+ }