@martel/calyx 1.7.0 → 1.9.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +71 -27
  3. package/benchmarks/graphql-benchmark.ts +81 -0
  4. package/benchmarks/index.ts +32 -0
  5. package/benchmarks/openapi-benchmark.ts +168 -0
  6. package/benchmarks/serialization-benchmark.ts +52 -0
  7. package/benchmarks/techniques-benchmark.ts +84 -0
  8. package/benchmarks/validation-benchmark.ts +74 -0
  9. package/bun.lock +14 -0
  10. package/package.json +8 -6
  11. package/src/cli/index.ts +19 -3
  12. package/src/compression/compression.middleware.ts +7 -0
  13. package/src/cookies/cookies.ts +69 -0
  14. package/src/database/mongoose.module.ts +250 -0
  15. package/src/database/typeorm.module.ts +276 -0
  16. package/src/file-upload/file-upload.interceptor.ts +93 -0
  17. package/src/file-upload/index.ts +1 -0
  18. package/src/graphql/decorators.ts +132 -0
  19. package/src/graphql/graphql.module.ts +316 -0
  20. package/src/graphql/index.ts +2 -0
  21. package/src/http/application.ts +380 -70
  22. package/src/http/factory.ts +1 -0
  23. package/src/http/router.ts +13 -0
  24. package/src/http-client/http-client.module.ts +124 -0
  25. package/src/http-client/index.ts +1 -0
  26. package/src/index.ts +15 -0
  27. package/src/logger/index.ts +1 -0
  28. package/src/logger/logger.service.ts +118 -0
  29. package/src/mvc/index.ts +1 -0
  30. package/src/mvc/mvc.ts +22 -0
  31. package/src/openapi/decorators.ts +203 -0
  32. package/src/openapi/index.ts +2 -0
  33. package/src/openapi/swagger.module.ts +326 -0
  34. package/src/queue/queue.module.ts +174 -0
  35. package/src/session/index.ts +1 -0
  36. package/src/session/session.middleware.ts +82 -0
  37. package/src/sse/index.ts +1 -0
  38. package/src/sse/sse.ts +18 -0
  39. package/src/streaming/index.ts +1 -0
  40. package/src/streaming/streamable-file.ts +32 -0
  41. package/src/validation/pipe.ts +79 -10
  42. package/src/versioning/versioning.ts +46 -0
  43. package/tests/graphql.test.ts +176 -0
  44. package/tests/openapi.test.ts +162 -0
  45. package/tests/techniques.test.ts +471 -0
@@ -0,0 +1,326 @@
1
+ import { CalyxApplication } from '../http/application.ts';
2
+
3
+ export class DocumentBuilder {
4
+ private document: any = {
5
+ openapi: '3.0.0',
6
+ info: {
7
+ title: 'Calyx Application',
8
+ version: '1.0.0',
9
+ description: '',
10
+ },
11
+ paths: {},
12
+ components: {
13
+ schemas: {},
14
+ securitySchemes: {},
15
+ },
16
+ };
17
+
18
+ setTitle(title: string) {
19
+ this.document.info.title = title;
20
+ return this;
21
+ }
22
+
23
+ setVersion(version: string) {
24
+ this.document.info.version = version;
25
+ return this;
26
+ }
27
+
28
+ setDescription(description: string) {
29
+ this.document.info.description = description;
30
+ return this;
31
+ }
32
+
33
+ addBearerAuth(options: any = { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, name = 'bearer') {
34
+ this.document.components.securitySchemes[name] = options;
35
+ return this;
36
+ }
37
+
38
+ addBasicAuth(options: any = { type: 'http', scheme: 'basic' }, name = 'basic') {
39
+ this.document.components.securitySchemes[name] = options;
40
+ return this;
41
+ }
42
+
43
+ addOAuth2(options: any = { type: 'oauth2', flows: {} }, name = 'oauth2') {
44
+ this.document.components.securitySchemes[name] = options;
45
+ return this;
46
+ }
47
+
48
+ addSecurity(name: string, scheme: any) {
49
+ this.document.components.securitySchemes[name] = scheme;
50
+ return this;
51
+ }
52
+
53
+ build() {
54
+ return this.document;
55
+ }
56
+ }
57
+
58
+ export class SwaggerModule {
59
+ static createDocument(app: CalyxApplication, config: any): any {
60
+ const document = { ...config };
61
+ if (!document.paths) document.paths = {};
62
+ if (!document.components) document.components = {};
63
+ if (!document.components.schemas) document.components.schemas = {};
64
+ if (!document.components.securitySchemes) {
65
+ document.components.securitySchemes = config.components?.securitySchemes || {};
66
+ }
67
+
68
+ const routes = app.getRoutes();
69
+
70
+ function registerSchema(typeClass: any) {
71
+ if (!typeClass || typeof typeClass !== 'function') return;
72
+ const schemaName = typeClass.name;
73
+ if (document.components.schemas[schemaName]) return;
74
+
75
+ const props = Reflect.getMetadata('calyx:api_properties', typeClass) || [];
76
+ const schemaProps: Record<string, any> = {};
77
+ const requiredProps: string[] = [];
78
+
79
+ for (const p of props) {
80
+ let pType = 'string';
81
+ if (p.type) {
82
+ pType = p.type.name ? p.type.name.toLowerCase() : String(p.type).toLowerCase();
83
+ }
84
+ schemaProps[p.propertyKey] = {
85
+ type: pType === 'number' || pType === 'boolean' || pType === 'object' || pType === 'array' ? pType : 'string',
86
+ description: p.description,
87
+ };
88
+ if (p.required) {
89
+ requiredProps.push(p.propertyKey);
90
+ }
91
+ }
92
+
93
+ document.components.schemas[schemaName] = {
94
+ type: 'object',
95
+ properties: schemaProps,
96
+ ...(requiredProps.length > 0 ? { required: requiredProps } : {}),
97
+ };
98
+ }
99
+
100
+ for (const route of routes) {
101
+ const { method, path, handler } = route;
102
+
103
+ const swaggerPath = path.replace(/:([a-zA-Z0-9_]+)/g, '{$1}');
104
+
105
+ if (!document.paths[swaggerPath]) {
106
+ document.paths[swaggerPath] = {};
107
+ }
108
+
109
+ const operationMeta =
110
+ Reflect.getMetadata('calyx:api_operation', handler.controllerClass.prototype, handler.methodName) || {};
111
+ const tags =
112
+ Reflect.getMetadata('calyx:api_tags', handler.controllerClass.prototype, handler.methodName) ||
113
+ Reflect.getMetadata('calyx:api_tags', handler.controllerClass) ||
114
+ [];
115
+ const responsesMeta =
116
+ Reflect.getMetadata('calyx:api_responses', handler.controllerClass.prototype, handler.methodName) || [];
117
+ const securityMeta =
118
+ Reflect.getMetadata('calyx:api_security', handler.controllerClass.prototype, handler.methodName) ||
119
+ Reflect.getMetadata('calyx:api_security', handler.controllerClass) ||
120
+ [];
121
+
122
+ // Extra Models
123
+ const extraModels = Reflect.getMetadata('calyx:api_extra_models', handler.controllerClass) || [];
124
+ for (const model of extraModels) {
125
+ registerSchema(model);
126
+ }
127
+
128
+ const parameters: any[] = [];
129
+
130
+ // Parse Path Params (Regex match)
131
+ const pathParams = [...path.matchAll(/:([a-zA-Z0-9_]+)/g)].map((m) => m[1]);
132
+ const pathParamDecorators = Reflect.getMetadata('calyx:api_params', handler.controllerClass.prototype, handler.methodName) || [];
133
+
134
+ for (const name of pathParams) {
135
+ const dec = pathParamDecorators.find((d: any) => d.name === name) || {};
136
+ parameters.push({
137
+ name,
138
+ in: 'path',
139
+ required: true,
140
+ description: dec.description || '',
141
+ schema: {
142
+ type: dec.type ? dec.type.name.toLowerCase() : 'string',
143
+ },
144
+ });
145
+ }
146
+
147
+ // Parse Http Parameter Decorators (@Query, @Headers, etc.)
148
+ const httpParams: any[] = Reflect.getMetadata('calyx:http_params', handler.controllerClass.prototype, handler.methodName) || [];
149
+ const paramTypes = Reflect.getMetadata('design:paramtypes', handler.controllerClass.prototype, handler.methodName) || [];
150
+ const apiQueries = Reflect.getMetadata('calyx:api_queries', handler.controllerClass.prototype, handler.methodName) || [];
151
+ const apiHeaders = Reflect.getMetadata('calyx:api_headers', handler.controllerClass.prototype, handler.methodName) || [];
152
+ let requestBody: any = undefined;
153
+
154
+ for (const param of httpParams) {
155
+ const paramType = paramTypes[param.index];
156
+
157
+ if (param.type === 'query') {
158
+ const qName = param.name;
159
+ if (qName) {
160
+ const dec = apiQueries.find((q: any) => q.name === qName) || {};
161
+ parameters.push({
162
+ name: qName,
163
+ in: 'query',
164
+ required: dec.required ?? false,
165
+ description: dec.description || '',
166
+ schema: {
167
+ type: dec.type ? dec.type.name.toLowerCase() : (paramType ? paramType.name.toLowerCase() : 'string'),
168
+ },
169
+ });
170
+ }
171
+ } else if (param.type === 'headers') {
172
+ const hName = param.name;
173
+ if (hName) {
174
+ const dec = apiHeaders.find((h: any) => h.name === hName) || {};
175
+ parameters.push({
176
+ name: hName,
177
+ in: 'header',
178
+ required: dec.required ?? false,
179
+ description: dec.description || '',
180
+ schema: {
181
+ type: 'string',
182
+ },
183
+ });
184
+ }
185
+ } else if (param.type === 'body') {
186
+ const bodyDec = Reflect.getMetadata('calyx:api_body', handler.controllerClass.prototype, handler.methodName) || {};
187
+ const targetType = bodyDec.type || paramType;
188
+
189
+ if (targetType) {
190
+ registerSchema(targetType);
191
+ requestBody = {
192
+ description: bodyDec.description || '',
193
+ required: bodyDec.required ?? true,
194
+ content: {
195
+ 'application/json': {
196
+ schema: {
197
+ $ref: `#/components/schemas/${targetType.name}`,
198
+ },
199
+ },
200
+ },
201
+ };
202
+ }
203
+ }
204
+ }
205
+
206
+ // Add manual @ApiQuery annotations if they weren't matched to method params
207
+ for (const q of apiQueries) {
208
+ if (!parameters.some((p) => p.in === 'query' && p.name === q.name)) {
209
+ parameters.push({
210
+ name: q.name,
211
+ in: 'query',
212
+ required: q.required ?? false,
213
+ description: q.description || '',
214
+ schema: {
215
+ type: q.type ? q.type.name.toLowerCase() : 'string',
216
+ },
217
+ });
218
+ }
219
+ }
220
+
221
+ // Add manual @ApiHeader annotations if they weren't matched to method params
222
+ for (const h of apiHeaders) {
223
+ if (!parameters.some((p) => p.in === 'header' && p.name === h.name)) {
224
+ parameters.push({
225
+ name: h.name,
226
+ in: 'header',
227
+ required: h.required ?? false,
228
+ description: h.description || '',
229
+ schema: {
230
+ type: 'string',
231
+ },
232
+ });
233
+ }
234
+ }
235
+
236
+ const responses: Record<string, any> = {};
237
+ if (responsesMeta.length > 0) {
238
+ for (const res of responsesMeta) {
239
+ responses[String(res.status)] = {
240
+ description: res.description,
241
+ };
242
+ if (res.type) {
243
+ const schemaName = res.type.name;
244
+ responses[String(res.status)].content = {
245
+ 'application/json': {
246
+ schema: { $ref: `#/components/schemas/${schemaName}` },
247
+ },
248
+ };
249
+ registerSchema(res.type);
250
+ }
251
+ }
252
+ } else {
253
+ responses['200'] = { description: 'OK' };
254
+ }
255
+
256
+ document.paths[swaggerPath][method.toLowerCase()] = {
257
+ summary: operationMeta.summary || '',
258
+ description: operationMeta.description || '',
259
+ tags,
260
+ parameters,
261
+ ...(requestBody ? { requestBody } : {}),
262
+ responses,
263
+ ...(securityMeta.length > 0 ? { security: securityMeta } : {}),
264
+ };
265
+ }
266
+
267
+ return document;
268
+ }
269
+
270
+ static setup(path: string, app: CalyxApplication, document: any) {
271
+ const jsonPath = `/${path}-json`.replace(/\/\/+/g, '/');
272
+ const uiPath = `/${path}`.replace(/\/\/+/g, '/');
273
+
274
+ app.use((req: any, res: any, next: any) => {
275
+ const url = new URL(req.url);
276
+ if (url.pathname === jsonPath && req.method === 'GET') {
277
+ res.status(200);
278
+ res.set('content-type', 'application/json');
279
+ res.send(JSON.stringify(document));
280
+ return;
281
+ }
282
+ next();
283
+ });
284
+
285
+ const html = `
286
+ <!DOCTYPE html>
287
+ <html lang="en">
288
+ <head>
289
+ <meta charset="utf-8" />
290
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
291
+ <title>Calyx Swagger UI</title>
292
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.11.0/swagger-ui.css" />
293
+ </head>
294
+ <body>
295
+ <div id="swagger-ui"></div>
296
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.11.0/swagger-ui-bundle.js"></script>
297
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.11.0/swagger-ui-standalone-preset.js"></script>
298
+ <script>
299
+ window.onload = () => {
300
+ window.ui = SwaggerUIBundle({
301
+ url: '${jsonPath}',
302
+ dom_id: '#swagger-ui',
303
+ presets: [
304
+ SwaggerUIBundle.presets.apis,
305
+ SwaggerUIStandalonePreset
306
+ ],
307
+ layout: "BaseLayout"
308
+ });
309
+ };
310
+ </script>
311
+ </body>
312
+ </html>
313
+ `;
314
+
315
+ app.use((req: any, res: any, next: any) => {
316
+ const url = new URL(req.url);
317
+ if (url.pathname === uiPath && req.method === 'GET') {
318
+ res.status(200);
319
+ res.set('content-type', 'text/html');
320
+ res.send(html);
321
+ return;
322
+ }
323
+ next();
324
+ });
325
+ }
326
+ }
@@ -0,0 +1,174 @@
1
+ import { Module, DynamicModule, Inject } from '../core/decorators.ts';
2
+ import { METADATA_KEYS } from '../core/metadata.ts';
3
+
4
+ export interface Job<T = any> {
5
+ id: string;
6
+ name: string;
7
+ data: T;
8
+ opts?: any;
9
+ status: 'waiting' | 'active' | 'completed' | 'failed';
10
+ result?: any;
11
+ error?: any;
12
+ }
13
+
14
+ export const PROCESSOR_METADATA_KEY = 'calyx:processor';
15
+ export const PROCESS_METADATA_KEY = 'calyx:process';
16
+
17
+ export function Processor(queueName?: string): ClassDecorator {
18
+ return (target) => {
19
+ Reflect.defineMetadata(PROCESSOR_METADATA_KEY, queueName ?? target.name, target);
20
+ Reflect.defineMetadata(METADATA_KEYS.INJECTABLE, true, target);
21
+ };
22
+ }
23
+
24
+ export interface ProcessOptions {
25
+ name?: string;
26
+ }
27
+
28
+ export function Process(options?: ProcessOptions | string): MethodDecorator {
29
+ return (target, propertyKey) => {
30
+ const name = typeof options === 'string' ? options : options?.name;
31
+ const existing = Reflect.getMetadata(PROCESS_METADATA_KEY, target.constructor) || [];
32
+ existing.push({ name: name ?? '', propertyKey });
33
+ Reflect.defineMetadata(PROCESS_METADATA_KEY, existing, target.constructor);
34
+ };
35
+ }
36
+
37
+ export function InjectQueue(name: string): ParameterDecorator & PropertyDecorator {
38
+ return Inject(`Queue_${name}`);
39
+ }
40
+
41
+ export class QueueManager {
42
+ private static processors = new Map<string, { instance: any; handlers: { name: string; propertyKey: string | symbol }[] }>();
43
+ private static queues = new Map<string, Queue>();
44
+
45
+ static registerProcessor(queueName: string, instance: any) {
46
+ const handlers = Reflect.getMetadata(PROCESS_METADATA_KEY, instance.constructor) || [];
47
+ this.processors.set(queueName, { instance, handlers });
48
+ }
49
+
50
+ static getOrCreateQueue(name: string): Queue {
51
+ let q = this.queues.get(name);
52
+ if (!q) {
53
+ q = new Queue(name);
54
+ this.queues.set(name, q);
55
+ }
56
+ return q;
57
+ }
58
+
59
+ static async dispatch(queueName: string, job: Job) {
60
+ const proc = this.processors.get(queueName);
61
+ if (!proc) {
62
+ // No processor registered yet, mark job as completed (dummy/mock queue mode)
63
+ job.status = 'completed';
64
+ return;
65
+ }
66
+
67
+ job.status = 'active';
68
+ const handler = proc.handlers.find((h) => h.name === job.name || (h.name === '' && !job.name));
69
+ if (handler) {
70
+ try {
71
+ const res = proc.instance[handler.propertyKey](job);
72
+ if (res instanceof Promise) {
73
+ job.result = await res;
74
+ } else {
75
+ job.result = res;
76
+ }
77
+ job.status = 'completed';
78
+ } catch (err: any) {
79
+ job.error = err;
80
+ job.status = 'failed';
81
+ }
82
+ } else {
83
+ // Fallback: match any handler or first one
84
+ const fallback = proc.handlers[0];
85
+ if (fallback) {
86
+ try {
87
+ const res = proc.instance[fallback.propertyKey](job);
88
+ if (res instanceof Promise) {
89
+ job.result = await res;
90
+ } else {
91
+ job.result = res;
92
+ }
93
+ job.status = 'completed';
94
+ } catch (err: any) {
95
+ job.error = err;
96
+ job.status = 'failed';
97
+ }
98
+ } else {
99
+ job.status = 'completed';
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ export class Queue {
106
+ private jobs: Job[] = [];
107
+ private jobCounter = 0;
108
+
109
+ constructor(public readonly name: string) {}
110
+
111
+ async add<T = any>(name: string, data: T, opts?: any): Promise<Job<T>>;
112
+ async add<T = any>(data: T, opts?: any): Promise<Job<T>>;
113
+ async add<T = any>(first: any, second?: any, third?: any): Promise<Job<T>> {
114
+ let name = '';
115
+ let data: T;
116
+ let opts: any;
117
+
118
+ if (typeof first === 'string') {
119
+ name = first;
120
+ data = second;
121
+ opts = third;
122
+ } else {
123
+ data = first;
124
+ opts = second;
125
+ }
126
+
127
+ this.jobCounter++;
128
+ const job: Job<T> = {
129
+ id: `${this.name}_job_${this.jobCounter}`,
130
+ name,
131
+ data,
132
+ opts,
133
+ status: 'waiting',
134
+ };
135
+
136
+ this.jobs.push(job);
137
+
138
+ // Run dispatch asynchronously in the background
139
+ queueMicrotask(() => {
140
+ QueueManager.dispatch(this.name, job);
141
+ });
142
+
143
+ return job;
144
+ }
145
+
146
+ async getJobs(): Promise<Job[]> {
147
+ return this.jobs;
148
+ }
149
+ }
150
+
151
+ @Module({})
152
+ export class QueueModule {
153
+ static registerQueue(...configs: { name: string }[]): DynamicModule {
154
+ const providers = configs.map((c) => {
155
+ return {
156
+ provide: `Queue_${c.name}`,
157
+ useFactory: () => {
158
+ return QueueManager.getOrCreateQueue(c.name);
159
+ },
160
+ };
161
+ });
162
+
163
+ return {
164
+ module: QueueModule,
165
+ providers,
166
+ exports: configs.map((c) => `Queue_${c.name}`),
167
+ };
168
+ }
169
+
170
+ // Automatic processor registration on initialization
171
+ onApplicationBootstrap() {
172
+ // Handled in CalyxApplication initialization dynamically
173
+ }
174
+ }
@@ -0,0 +1 @@
1
+ export * from './session.middleware.ts';
@@ -0,0 +1,82 @@
1
+ import { createParamDecorator } from '../http/decorators.ts';
2
+ import { formatCookie } from '../cookies/cookies.ts';
3
+
4
+ export interface SessionData {
5
+ id: string;
6
+ data: Record<string, any>;
7
+ expiresAt: number;
8
+ }
9
+
10
+ export class MemorySessionStore {
11
+ private store = new Map<string, SessionData>();
12
+
13
+ get(sid: string): SessionData | undefined {
14
+ const session = this.store.get(sid);
15
+ if (!session) return undefined;
16
+ if (session.expiresAt < Date.now()) {
17
+ this.store.delete(sid);
18
+ return undefined;
19
+ }
20
+ return session;
21
+ }
22
+
23
+ set(sid: string, data: Record<string, any>, ttlSeconds: number) {
24
+ this.store.set(sid, {
25
+ id: sid,
26
+ data,
27
+ expiresAt: Date.now() + ttlSeconds * 1000,
28
+ });
29
+ }
30
+
31
+ destroy(sid: string) {
32
+ this.store.delete(sid);
33
+ }
34
+ }
35
+
36
+ export const sessionStore = new MemorySessionStore();
37
+
38
+ export interface SessionOptions {
39
+ secret?: string;
40
+ name?: string;
41
+ ttl?: number; // TTL in seconds, default 1 day
42
+ }
43
+
44
+ export function session(options: SessionOptions = {}) {
45
+ const cookieName = options.name ?? 'calyx_sid';
46
+ const ttl = options.ttl ?? 86400; // 1 day
47
+
48
+ return (req: any, res: any, next: any) => {
49
+ const cookies = req.cookies || {};
50
+ let sid = cookies[cookieName];
51
+ let sessionData = sid ? sessionStore.get(sid) : undefined;
52
+
53
+ if (!sessionData) {
54
+ sid = crypto.randomUUID();
55
+ sessionData = {
56
+ id: sid,
57
+ data: {},
58
+ expiresAt: Date.now() + ttl * 1000,
59
+ };
60
+ sessionStore.set(sid, sessionData.data, ttl);
61
+ }
62
+
63
+ req.session = sessionData.data;
64
+
65
+ // Attach finalizeSession hook to request
66
+ req.finalizeSession = (responseHeaders: Headers) => {
67
+ sessionStore.set(sid, req.session, ttl);
68
+ responseHeaders.append('Set-Cookie', formatCookie(cookieName, sid, {
69
+ maxAge: ttl,
70
+ httpOnly: true,
71
+ path: '/',
72
+ }));
73
+ };
74
+
75
+ next();
76
+ };
77
+ }
78
+
79
+ export const Session = createParamDecorator((data, ctx) => {
80
+ const req = ctx.switchToHttp().getRequest();
81
+ return (req as any).session;
82
+ });
@@ -0,0 +1 @@
1
+ export * from './sse.ts';
package/src/sse/sse.ts ADDED
@@ -0,0 +1,18 @@
1
+ import 'reflect-metadata';
2
+ import { METADATA_KEYS } from '../core/metadata.ts';
3
+
4
+ export interface MessageEvent {
5
+ data: string | object;
6
+ id?: string;
7
+ type?: string;
8
+ retry?: number;
9
+ }
10
+
11
+ export function Sse(path = ''): MethodDecorator {
12
+ return (target: any, propertyKey: string | symbol, descriptor: any) => {
13
+ // SSE routes are GET requests
14
+ Reflect.defineMetadata(METADATA_KEYS.HTTP_METHOD, { method: 'GET', path }, target, propertyKey);
15
+ Reflect.defineMetadata('calyx:sse', true, target, propertyKey);
16
+ return descriptor;
17
+ };
18
+ }
@@ -0,0 +1 @@
1
+ export * from './streamable-file.ts';
@@ -0,0 +1,32 @@
1
+ export interface StreamableFileOptions {
2
+ type?: string;
3
+ disposition?: string;
4
+ length?: number;
5
+ }
6
+
7
+ export class StreamableFile {
8
+ constructor(
9
+ public readonly streamOrBuffer: any,
10
+ public readonly options: StreamableFileOptions = {}
11
+ ) {}
12
+
13
+ getStream(): any {
14
+ return this.streamOrBuffer;
15
+ }
16
+
17
+ getHeaders(): Record<string, string> {
18
+ const headers: Record<string, string> = {};
19
+ if (this.options.type) {
20
+ headers['content-type'] = this.options.type;
21
+ } else {
22
+ headers['content-type'] = 'application/octet-stream';
23
+ }
24
+ if (this.options.disposition) {
25
+ headers['content-disposition'] = this.options.disposition;
26
+ }
27
+ if (this.options.length !== undefined) {
28
+ headers['content-length'] = String(this.options.length);
29
+ }
30
+ return headers;
31
+ }
32
+ }