adorn-api 1.1.11 → 1.1.13

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 (75) hide show
  1. package/README.md +18 -0
  2. package/dist/adapter/express/types.d.ts +3 -46
  3. package/dist/adapter/fastify/coercion.d.ts +12 -0
  4. package/dist/adapter/fastify/coercion.js +289 -0
  5. package/dist/adapter/fastify/controllers.d.ts +7 -0
  6. package/dist/adapter/fastify/controllers.js +201 -0
  7. package/dist/adapter/fastify/index.d.ts +14 -0
  8. package/dist/adapter/fastify/index.js +67 -0
  9. package/dist/adapter/fastify/multipart.d.ts +26 -0
  10. package/dist/adapter/fastify/multipart.js +75 -0
  11. package/dist/adapter/fastify/openapi.d.ts +10 -0
  12. package/dist/adapter/fastify/openapi.js +76 -0
  13. package/dist/adapter/fastify/response-serializer.d.ts +2 -0
  14. package/dist/adapter/fastify/response-serializer.js +162 -0
  15. package/dist/adapter/fastify/types.d.ts +100 -0
  16. package/dist/adapter/fastify/types.js +2 -0
  17. package/dist/adapter/metal-orm/index.d.ts +1 -1
  18. package/dist/adapter/metal-orm/types.d.ts +23 -0
  19. package/dist/adapter/native/coercion.d.ts +12 -0
  20. package/dist/adapter/native/coercion.js +289 -0
  21. package/dist/adapter/native/controllers.d.ts +17 -0
  22. package/dist/adapter/native/controllers.js +215 -0
  23. package/dist/adapter/native/index.d.ts +14 -0
  24. package/dist/adapter/native/index.js +127 -0
  25. package/dist/adapter/native/openapi.d.ts +7 -0
  26. package/dist/adapter/native/openapi.js +82 -0
  27. package/dist/adapter/native/response-serializer.d.ts +5 -0
  28. package/dist/adapter/native/response-serializer.js +160 -0
  29. package/dist/adapter/native/router.d.ts +25 -0
  30. package/dist/adapter/native/router.js +68 -0
  31. package/dist/adapter/native/types.d.ts +77 -0
  32. package/dist/adapter/native/types.js +2 -0
  33. package/dist/core/auth.d.ts +11 -12
  34. package/dist/core/auth.js +2 -2
  35. package/dist/core/logger.d.ts +3 -4
  36. package/dist/core/logger.js +2 -2
  37. package/dist/core/streaming.d.ts +10 -10
  38. package/dist/core/streaming.js +31 -19
  39. package/dist/core/types.d.ts +102 -0
  40. package/dist/index.d.ts +6 -1
  41. package/dist/index.js +16 -1
  42. package/examples/fastify/app.ts +16 -0
  43. package/examples/fastify/index.ts +21 -0
  44. package/package.json +24 -18
  45. package/src/adapter/express/controllers.ts +249 -249
  46. package/src/adapter/express/types.ts +121 -160
  47. package/src/adapter/fastify/coercion.ts +369 -0
  48. package/src/adapter/fastify/controllers.ts +255 -0
  49. package/src/adapter/fastify/index.ts +53 -0
  50. package/src/adapter/fastify/multipart.ts +94 -0
  51. package/src/adapter/fastify/openapi.ts +93 -0
  52. package/src/adapter/fastify/response-serializer.ts +179 -0
  53. package/src/adapter/fastify/types.ts +119 -0
  54. package/src/adapter/metal-orm/index.ts +3 -0
  55. package/src/adapter/metal-orm/types.ts +25 -0
  56. package/src/adapter/native/coercion.ts +369 -0
  57. package/src/adapter/native/controllers.ts +271 -0
  58. package/src/adapter/native/index.ts +116 -0
  59. package/src/adapter/native/openapi.ts +109 -0
  60. package/src/adapter/native/response-serializer.ts +177 -0
  61. package/src/adapter/native/router.ts +90 -0
  62. package/src/adapter/native/types.ts +96 -0
  63. package/src/core/auth.ts +314 -315
  64. package/src/core/health.ts +234 -235
  65. package/src/core/logger.ts +245 -247
  66. package/src/core/streaming.ts +342 -330
  67. package/src/core/types.ts +115 -0
  68. package/src/index.ts +46 -16
  69. package/tests/e2e/fastify.e2e.test.ts +174 -0
  70. package/tests/native.test.ts +191 -0
  71. package/tests/typecheck/query-params.typecheck.ts +42 -0
  72. package/tests/unit/openapi-parameters.test.ts +97 -97
  73. package/tsconfig.json +14 -13
  74. package/tsconfig.typecheck.json +8 -0
  75. package/vitest.config.ts +47 -7
@@ -1,330 +1,342 @@
1
- import type { Response } from "express";
2
-
3
- /**
4
- * Server-Sent Event data structure.
5
- */
6
- export interface SseEvent {
7
- /** Event data (will be JSON stringified if object) */
8
- data: unknown;
9
- /** Optional event type/name */
10
- event?: string;
11
- /** Optional event ID */
12
- id?: string;
13
- /** Optional retry interval in milliseconds */
14
- retry?: number;
15
- }
16
-
17
- /**
18
- * Options for SSE emitter configuration.
19
- */
20
- export interface SseEmitterOptions {
21
- /** Keep-alive interval in milliseconds (default: 15000) */
22
- keepAliveInterval?: number;
23
- /** Whether to send keep-alive comments (default: true) */
24
- keepAlive?: boolean;
25
- }
26
-
27
- /**
28
- * SSE Emitter for sending Server-Sent Events to clients.
29
- */
30
- export class SseEmitter {
31
- private closed = false;
32
- private keepAliveTimer?: ReturnType<typeof setInterval>;
33
-
34
- constructor(
35
- private readonly res: Response,
36
- options: SseEmitterOptions = {}
37
- ) {
38
- this.setupSseHeaders();
39
-
40
- const { keepAlive = true, keepAliveInterval = 15000 } = options;
41
- if (keepAlive) {
42
- this.startKeepAlive(keepAliveInterval);
43
- }
44
-
45
- res.on("close", () => {
46
- this.close();
47
- });
48
- }
49
-
50
- /**
51
- * Send an SSE event to the client.
52
- */
53
- send(eventOrData: SseEvent | unknown): void {
54
- if (this.closed) {
55
- return;
56
- }
57
-
58
- const event = this.normalizeEvent(eventOrData);
59
- const message = this.formatEvent(event);
60
- this.res.write(message);
61
- }
62
-
63
- /**
64
- * Send data with a specific event type.
65
- */
66
- emit(eventType: string, data: unknown): void {
67
- this.send({ event: eventType, data });
68
- }
69
-
70
- /**
71
- * Send a comment (for keep-alive or debugging).
72
- */
73
- comment(text: string): void {
74
- if (this.closed) {
75
- return;
76
- }
77
- this.res.write(`: ${text}\n\n`);
78
- }
79
-
80
- /**
81
- * Close the SSE connection.
82
- */
83
- close(): void {
84
- if (this.closed) {
85
- return;
86
- }
87
- this.closed = true;
88
-
89
- if (this.keepAliveTimer) {
90
- clearInterval(this.keepAliveTimer);
91
- this.keepAliveTimer = undefined;
92
- }
93
-
94
- if (!this.res.writableEnded) {
95
- this.res.end();
96
- }
97
- }
98
-
99
- /**
100
- * Check if the connection is closed.
101
- */
102
- isClosed(): boolean {
103
- return this.closed;
104
- }
105
-
106
- private setupSseHeaders(): void {
107
- this.res.setHeader("Content-Type", "text/event-stream");
108
- this.res.setHeader("Cache-Control", "no-cache, no-transform");
109
- this.res.setHeader("Connection", "keep-alive");
110
- this.res.setHeader("X-Accel-Buffering", "no");
111
- this.res.flushHeaders();
112
- }
113
-
114
- private startKeepAlive(interval: number): void {
115
- this.keepAliveTimer = setInterval(() => {
116
- if (!this.closed) {
117
- this.comment("keep-alive");
118
- }
119
- }, interval);
120
- }
121
-
122
- private normalizeEvent(eventOrData: SseEvent | unknown): SseEvent {
123
- if (this.isSseEvent(eventOrData)) {
124
- return eventOrData;
125
- }
126
- return { data: eventOrData };
127
- }
128
-
129
- private isSseEvent(value: unknown): value is SseEvent {
130
- return (
131
- value !== null &&
132
- typeof value === "object" &&
133
- "data" in value
134
- );
135
- }
136
-
137
- private formatEvent(event: SseEvent): string {
138
- let message = "";
139
-
140
- if (event.id !== undefined) {
141
- message += `id: ${event.id}\n`;
142
- }
143
-
144
- if (event.event !== undefined) {
145
- message += `event: ${event.event}\n`;
146
- }
147
-
148
- if (event.retry !== undefined) {
149
- message += `retry: ${event.retry}\n`;
150
- }
151
-
152
- const data = typeof event.data === "string"
153
- ? event.data
154
- : JSON.stringify(event.data);
155
-
156
- const lines = data.split("\n");
157
- for (const line of lines) {
158
- message += `data: ${line}\n`;
159
- }
160
-
161
- message += "\n";
162
- return message;
163
- }
164
- }
165
-
166
- /**
167
- * Create an SSE emitter from an Express response.
168
- */
169
- export function createSseEmitter(res: Response, options?: SseEmitterOptions): SseEmitter {
170
- return new SseEmitter(res, options);
171
- }
172
-
173
- /**
174
- * Options for streaming response configuration.
175
- */
176
- export interface StreamOptions {
177
- /** Content type for the stream (default: application/octet-stream) */
178
- contentType?: string;
179
- /** Custom headers */
180
- headers?: Record<string, string>;
181
- }
182
-
183
- /**
184
- * Stream writer for sending chunked data to clients.
185
- */
186
- export class StreamWriter {
187
- private closed = false;
188
-
189
- constructor(
190
- private readonly res: Response,
191
- options: StreamOptions = {}
192
- ) {
193
- this.setupHeaders(options);
194
-
195
- res.on("close", () => {
196
- this.closed = true;
197
- });
198
- }
199
-
200
- /**
201
- * Write data to the stream.
202
- */
203
- write(data: string | Buffer): boolean {
204
- if (this.closed) {
205
- return false;
206
- }
207
- return this.res.write(data);
208
- }
209
-
210
- /**
211
- * Write a line to the stream (adds newline).
212
- */
213
- writeLine(data: string): boolean {
214
- return this.write(data + "\n");
215
- }
216
-
217
- /**
218
- * Write JSON data to the stream.
219
- */
220
- writeJson(data: unknown): boolean {
221
- return this.write(JSON.stringify(data));
222
- }
223
-
224
- /**
225
- * Write JSON data followed by a newline (NDJSON format).
226
- */
227
- writeJsonLine(data: unknown): boolean {
228
- return this.writeLine(JSON.stringify(data));
229
- }
230
-
231
- /**
232
- * Close the stream.
233
- */
234
- close(): void {
235
- if (this.closed) {
236
- return;
237
- }
238
- this.closed = true;
239
-
240
- if (!this.res.writableEnded) {
241
- this.res.end();
242
- }
243
- }
244
-
245
- /**
246
- * Check if the stream is closed.
247
- */
248
- isClosed(): boolean {
249
- return this.closed;
250
- }
251
-
252
- private setupHeaders(options: StreamOptions): void {
253
- const contentType = options.contentType ?? "application/octet-stream";
254
- this.res.setHeader("Content-Type", contentType);
255
- this.res.setHeader("Transfer-Encoding", "chunked");
256
- this.res.setHeader("Cache-Control", "no-cache, no-transform");
257
- this.res.setHeader("X-Accel-Buffering", "no");
258
-
259
- if (options.headers) {
260
- for (const [key, value] of Object.entries(options.headers)) {
261
- this.res.setHeader(key, value);
262
- }
263
- }
264
-
265
- this.res.flushHeaders();
266
- }
267
- }
268
-
269
- /**
270
- * Create a stream writer from an Express response.
271
- */
272
- export function createStreamWriter(res: Response, options?: StreamOptions): StreamWriter {
273
- return new StreamWriter(res, options);
274
- }
275
-
276
- /**
277
- * Create an NDJSON stream writer (newline-delimited JSON).
278
- */
279
- export function createNdjsonStream(res: Response): StreamWriter {
280
- return new StreamWriter(res, { contentType: "application/x-ndjson" });
281
- }
282
-
283
- /**
284
- * Stream an async iterable to the response.
285
- */
286
- export async function streamIterable<T>(
287
- res: Response,
288
- iterable: AsyncIterable<T>,
289
- options: StreamOptions & { transform?: (item: T) => string | Buffer } = {}
290
- ): Promise<void> {
291
- const writer = new StreamWriter(res, options);
292
- const transform = options.transform ?? ((item: T) => JSON.stringify(item) + "\n");
293
-
294
- try {
295
- for await (const item of iterable) {
296
- if (writer.isClosed()) {
297
- break;
298
- }
299
- writer.write(transform(item));
300
- }
301
- } finally {
302
- writer.close();
303
- }
304
- }
305
-
306
- /**
307
- * Stream an async iterable as SSE events.
308
- */
309
- export async function streamSseIterable<T>(
310
- res: Response,
311
- iterable: AsyncIterable<T>,
312
- options: SseEmitterOptions & { eventType?: string } = {}
313
- ): Promise<void> {
314
- const emitter = new SseEmitter(res, options);
315
-
316
- try {
317
- for await (const item of iterable) {
318
- if (emitter.isClosed()) {
319
- break;
320
- }
321
- if (options.eventType) {
322
- emitter.emit(options.eventType, item);
323
- } else {
324
- emitter.send(item);
325
- }
326
- }
327
- } finally {
328
- emitter.close();
329
- }
330
- }
1
+ import type { SseEmitterInterface, StreamWriterInterface } from "./types";
2
+
3
+ /**
4
+ * Server-Sent Event data structure.
5
+ */
6
+ export interface SseEvent {
7
+ /** Event data (will be JSON stringified if object) */
8
+ data: unknown;
9
+ /** Optional event type/name */
10
+ event?: string;
11
+ /** Optional event ID */
12
+ id?: string;
13
+ /** Optional retry interval in milliseconds */
14
+ retry?: number;
15
+ }
16
+
17
+ /**
18
+ * Options for SSE emitter configuration.
19
+ */
20
+ export interface SseEmitterOptions {
21
+ /** Keep-alive interval in milliseconds (default: 15000) */
22
+ keepAliveInterval?: number;
23
+ /** Whether to send keep-alive comments (default: true) */
24
+ keepAlive?: boolean;
25
+ }
26
+
27
+ /**
28
+ * SSE Emitter for sending Server-Sent Events to clients.
29
+ */
30
+ export class SseEmitter implements SseEmitterInterface {
31
+ private closed = false;
32
+ private keepAliveTimer?: ReturnType<typeof setInterval>;
33
+
34
+ constructor(
35
+ private readonly res: any,
36
+ options: SseEmitterOptions = {}
37
+ ) {
38
+ this.setupSseHeaders();
39
+
40
+ const { keepAlive = true, keepAliveInterval = 15000 } = options;
41
+ if (keepAlive) {
42
+ this.startKeepAlive(keepAliveInterval);
43
+ }
44
+
45
+ if (res.on) {
46
+ res.on("close", () => {
47
+ this.close();
48
+ });
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Send an SSE event to the client.
54
+ */
55
+ send(eventOrData: SseEvent | unknown): void {
56
+ if (this.closed) {
57
+ return;
58
+ }
59
+
60
+ const event = this.normalizeEvent(eventOrData);
61
+ const message = this.formatEvent(event);
62
+ this.res.write(message);
63
+ }
64
+
65
+ /**
66
+ * Send data with a specific event type.
67
+ */
68
+ emit(eventType: string, data: unknown): void {
69
+ this.send({ event: eventType, data });
70
+ }
71
+
72
+ /**
73
+ * Send a comment (for keep-alive or debugging).
74
+ */
75
+ comment(text: string): void {
76
+ if (this.closed) {
77
+ return;
78
+ }
79
+ this.res.write(`: ${text}\n\n`);
80
+ }
81
+
82
+ /**
83
+ * Close the SSE connection.
84
+ */
85
+ close(): void {
86
+ if (this.closed) {
87
+ return;
88
+ }
89
+ this.closed = true;
90
+
91
+ if (this.keepAliveTimer) {
92
+ clearInterval(this.keepAliveTimer);
93
+ this.keepAliveTimer = undefined;
94
+ }
95
+
96
+ if (!this.res.writableEnded) {
97
+ this.res.end();
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Check if the connection is closed.
103
+ */
104
+ isClosed(): boolean {
105
+ return this.closed;
106
+ }
107
+
108
+ private setupSseHeaders(): void {
109
+ if (this.res.setHeader) {
110
+ this.res.setHeader("Content-Type", "text/event-stream");
111
+ this.res.setHeader("Cache-Control", "no-cache, no-transform");
112
+ this.res.setHeader("Connection", "keep-alive");
113
+ this.res.setHeader("X-Accel-Buffering", "no");
114
+ }
115
+ if (this.res.flushHeaders) {
116
+ this.res.flushHeaders();
117
+ }
118
+ }
119
+
120
+ private startKeepAlive(interval: number): void {
121
+ this.keepAliveTimer = setInterval(() => {
122
+ if (!this.closed) {
123
+ this.comment("keep-alive");
124
+ }
125
+ }, interval);
126
+ }
127
+
128
+ private normalizeEvent(eventOrData: SseEvent | unknown): SseEvent {
129
+ if (this.isSseEvent(eventOrData)) {
130
+ return eventOrData;
131
+ }
132
+ return { data: eventOrData };
133
+ }
134
+
135
+ private isSseEvent(value: unknown): value is SseEvent {
136
+ return (
137
+ value !== null &&
138
+ typeof value === "object" &&
139
+ "data" in value
140
+ );
141
+ }
142
+
143
+ private formatEvent(event: SseEvent): string {
144
+ let message = "";
145
+
146
+ if (event.id !== undefined) {
147
+ message += `id: ${event.id}\n`;
148
+ }
149
+
150
+ if (event.event !== undefined) {
151
+ message += `event: ${event.event}\n`;
152
+ }
153
+
154
+ if (event.retry !== undefined) {
155
+ message += `retry: ${event.retry}\n`;
156
+ }
157
+
158
+ const data = typeof event.data === "string"
159
+ ? event.data
160
+ : JSON.stringify(event.data);
161
+
162
+ const lines = data.split("\n");
163
+ for (const line of lines) {
164
+ message += `data: ${line}\n`;
165
+ }
166
+
167
+ message += "\n";
168
+ return message;
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Create an SSE emitter from an Express response.
174
+ */
175
+ export function createSseEmitter(res: any, options?: SseEmitterOptions): SseEmitter {
176
+ return new SseEmitter(res, options);
177
+ }
178
+
179
+ /**
180
+ * Options for streaming response configuration.
181
+ */
182
+ export interface StreamOptions {
183
+ /** Content type for the stream (default: application/octet-stream) */
184
+ contentType?: string;
185
+ /** Custom headers */
186
+ headers?: Record<string, string>;
187
+ }
188
+
189
+ /**
190
+ * Stream writer for sending chunked data to clients.
191
+ */
192
+ export class StreamWriter implements StreamWriterInterface {
193
+ private closed = false;
194
+
195
+ constructor(
196
+ private readonly res: any,
197
+ options: StreamOptions = {}
198
+ ) {
199
+ this.setupHeaders(options);
200
+
201
+ if (res.on) {
202
+ res.on("close", () => {
203
+ this.closed = true;
204
+ });
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Write data to the stream.
210
+ */
211
+ write(data: string | Buffer): boolean {
212
+ if (this.closed) {
213
+ return false;
214
+ }
215
+ return this.res.write(data);
216
+ }
217
+
218
+ /**
219
+ * Write a line to the stream (adds newline).
220
+ */
221
+ writeLine(data: string): boolean {
222
+ return this.write(data + "\n");
223
+ }
224
+
225
+ /**
226
+ * Write JSON data to the stream.
227
+ */
228
+ writeJson(data: unknown): boolean {
229
+ return this.write(JSON.stringify(data));
230
+ }
231
+
232
+ /**
233
+ * Write JSON data followed by a newline (NDJSON format).
234
+ */
235
+ writeJsonLine(data: unknown): boolean {
236
+ return this.writeLine(JSON.stringify(data));
237
+ }
238
+
239
+ /**
240
+ * Close the stream.
241
+ */
242
+ close(): void {
243
+ if (this.closed) {
244
+ return;
245
+ }
246
+ this.closed = true;
247
+
248
+ if (!this.res.writableEnded) {
249
+ this.res.end();
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Check if the stream is closed.
255
+ */
256
+ isClosed(): boolean {
257
+ return this.closed;
258
+ }
259
+
260
+ private setupHeaders(options: StreamOptions): void {
261
+ const contentType = options.contentType ?? "application/octet-stream";
262
+ if (this.res.setHeader) {
263
+ this.res.setHeader("Content-Type", contentType);
264
+ this.res.setHeader("Transfer-Encoding", "chunked");
265
+ this.res.setHeader("Cache-Control", "no-cache, no-transform");
266
+ this.res.setHeader("X-Accel-Buffering", "no");
267
+
268
+ if (options.headers) {
269
+ for (const [key, value] of Object.entries(options.headers)) {
270
+ this.res.setHeader(key, value);
271
+ }
272
+ }
273
+ }
274
+
275
+ if (this.res.flushHeaders) {
276
+ this.res.flushHeaders();
277
+ }
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Create a stream writer from an Express response.
283
+ */
284
+ export function createStreamWriter(res: any, options?: StreamOptions): StreamWriter {
285
+ return new StreamWriter(res, options);
286
+ }
287
+
288
+ /**
289
+ * Create an NDJSON stream writer (newline-delimited JSON).
290
+ */
291
+ export function createNdjsonStream(res: any): StreamWriter {
292
+ return new StreamWriter(res, { contentType: "application/x-ndjson" });
293
+ }
294
+
295
+ /**
296
+ * Stream an async iterable to the response.
297
+ */
298
+ export async function streamIterable<T>(
299
+ res: any,
300
+ iterable: AsyncIterable<T>,
301
+ options: StreamOptions & { transform?: (item: T) => string | Buffer } = {}
302
+ ): Promise<void> {
303
+ const writer = new StreamWriter(res, options);
304
+ const transform = options.transform ?? ((item: T) => JSON.stringify(item) + "\n");
305
+
306
+ try {
307
+ for await (const item of iterable) {
308
+ if (writer.isClosed()) {
309
+ break;
310
+ }
311
+ writer.write(transform(item));
312
+ }
313
+ } finally {
314
+ writer.close();
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Stream an async iterable as SSE events.
320
+ */
321
+ export async function streamSseIterable<T>(
322
+ res: any,
323
+ iterable: AsyncIterable<T>,
324
+ options: SseEmitterOptions & { eventType?: string } = {}
325
+ ): Promise<void> {
326
+ const emitter = new SseEmitter(res, options);
327
+
328
+ try {
329
+ for await (const item of iterable) {
330
+ if (emitter.isClosed()) {
331
+ break;
332
+ }
333
+ if (options.eventType) {
334
+ emitter.emit(options.eventType, item);
335
+ } else {
336
+ emitter.send(item);
337
+ }
338
+ }
339
+ } finally {
340
+ emitter.close();
341
+ }
342
+ }