@terreno/api 0.0.1

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 (119) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +170 -0
  3. package/biome.jsonc +22 -0
  4. package/bunfig.toml +4 -0
  5. package/dist/api.d.ts +227 -0
  6. package/dist/api.js +1024 -0
  7. package/dist/api.test.d.ts +1 -0
  8. package/dist/api.test.js +2143 -0
  9. package/dist/auth.d.ts +50 -0
  10. package/dist/auth.js +512 -0
  11. package/dist/auth.test.d.ts +1 -0
  12. package/dist/auth.test.js +778 -0
  13. package/dist/errors.d.ts +75 -0
  14. package/dist/errors.js +216 -0
  15. package/dist/example.d.ts +1 -0
  16. package/dist/example.js +118 -0
  17. package/dist/expressServer.d.ts +35 -0
  18. package/dist/expressServer.js +436 -0
  19. package/dist/index.d.ts +14 -0
  20. package/dist/index.js +30 -0
  21. package/dist/logger.d.ts +23 -0
  22. package/dist/logger.js +249 -0
  23. package/dist/middleware.d.ts +10 -0
  24. package/dist/middleware.js +52 -0
  25. package/dist/notifiers/googleChatNotifier.d.ts +5 -0
  26. package/dist/notifiers/googleChatNotifier.js +130 -0
  27. package/dist/notifiers/googleChatNotifier.test.d.ts +1 -0
  28. package/dist/notifiers/googleChatNotifier.test.js +260 -0
  29. package/dist/notifiers/index.d.ts +3 -0
  30. package/dist/notifiers/index.js +19 -0
  31. package/dist/notifiers/slackNotifier.d.ts +5 -0
  32. package/dist/notifiers/slackNotifier.js +130 -0
  33. package/dist/notifiers/slackNotifier.test.d.ts +1 -0
  34. package/dist/notifiers/slackNotifier.test.js +259 -0
  35. package/dist/notifiers/zoomNotifier.d.ts +34 -0
  36. package/dist/notifiers/zoomNotifier.js +181 -0
  37. package/dist/notifiers/zoomNotifier.test.d.ts +1 -0
  38. package/dist/notifiers/zoomNotifier.test.js +370 -0
  39. package/dist/openApi.d.ts +60 -0
  40. package/dist/openApi.js +441 -0
  41. package/dist/openApi.test.d.ts +1 -0
  42. package/dist/openApi.test.js +445 -0
  43. package/dist/openApiBuilder.d.ts +419 -0
  44. package/dist/openApiBuilder.js +424 -0
  45. package/dist/openApiBuilder.test.d.ts +1 -0
  46. package/dist/openApiBuilder.test.js +509 -0
  47. package/dist/openApiEtag.d.ts +7 -0
  48. package/dist/openApiEtag.js +38 -0
  49. package/dist/permissions.d.ts +26 -0
  50. package/dist/permissions.js +331 -0
  51. package/dist/permissions.test.d.ts +1 -0
  52. package/dist/permissions.test.js +413 -0
  53. package/dist/plugins.d.ts +67 -0
  54. package/dist/plugins.js +315 -0
  55. package/dist/plugins.test.d.ts +1 -0
  56. package/dist/plugins.test.js +639 -0
  57. package/dist/populate.d.ts +14 -0
  58. package/dist/populate.js +315 -0
  59. package/dist/populate.test.d.ts +1 -0
  60. package/dist/populate.test.js +133 -0
  61. package/dist/response.d.ts +0 -0
  62. package/dist/response.js +1 -0
  63. package/dist/tests/bunSetup.d.ts +1 -0
  64. package/dist/tests/bunSetup.js +297 -0
  65. package/dist/tests/index.d.ts +1 -0
  66. package/dist/tests/index.js +17 -0
  67. package/dist/tests.d.ts +99 -0
  68. package/dist/tests.js +273 -0
  69. package/dist/transformers.d.ts +25 -0
  70. package/dist/transformers.js +217 -0
  71. package/dist/transformers.test.d.ts +1 -0
  72. package/dist/transformers.test.js +370 -0
  73. package/dist/utils.d.ts +11 -0
  74. package/dist/utils.js +143 -0
  75. package/dist/utils.test.d.ts +1 -0
  76. package/dist/utils.test.js +14 -0
  77. package/index.ts +1 -0
  78. package/package.json +88 -0
  79. package/src/__snapshots__/openApi.test.ts.snap +4814 -0
  80. package/src/__snapshots__/openApiBuilder.test.ts.snap +1485 -0
  81. package/src/api.test.ts +1661 -0
  82. package/src/api.ts +1036 -0
  83. package/src/auth.test.ts +550 -0
  84. package/src/auth.ts +408 -0
  85. package/src/errors.ts +225 -0
  86. package/src/example.ts +99 -0
  87. package/src/express.d.ts +5 -0
  88. package/src/expressServer.ts +387 -0
  89. package/src/index.ts +14 -0
  90. package/src/logger.ts +190 -0
  91. package/src/middleware.ts +18 -0
  92. package/src/notifiers/googleChatNotifier.test.ts +114 -0
  93. package/src/notifiers/googleChatNotifier.ts +47 -0
  94. package/src/notifiers/index.ts +3 -0
  95. package/src/notifiers/slackNotifier.test.ts +113 -0
  96. package/src/notifiers/slackNotifier.ts +55 -0
  97. package/src/notifiers/zoomNotifier.test.ts +207 -0
  98. package/src/notifiers/zoomNotifier.ts +111 -0
  99. package/src/openApi.test.ts +331 -0
  100. package/src/openApi.ts +494 -0
  101. package/src/openApiBuilder.test.ts +442 -0
  102. package/src/openApiBuilder.ts +636 -0
  103. package/src/openApiEtag.ts +40 -0
  104. package/src/permissions.test.ts +219 -0
  105. package/src/permissions.ts +228 -0
  106. package/src/plugins.test.ts +390 -0
  107. package/src/plugins.ts +289 -0
  108. package/src/populate.test.ts +65 -0
  109. package/src/populate.ts +258 -0
  110. package/src/response.ts +0 -0
  111. package/src/tests/bunSetup.ts +234 -0
  112. package/src/tests/index.ts +1 -0
  113. package/src/tests.ts +218 -0
  114. package/src/transformers.test.ts +202 -0
  115. package/src/transformers.ts +170 -0
  116. package/src/utils.test.ts +14 -0
  117. package/src/utils.ts +47 -0
  118. package/tsconfig.json +60 -0
  119. package/types.d.ts +17 -0
@@ -0,0 +1,636 @@
1
+ /**
2
+ * OpenAPI Middleware Builder
3
+ *
4
+ * This module provides a fluent builder pattern for constructing OpenAPI middleware
5
+ * for Express routes that don't directly map to Mongoose models. It allows you to
6
+ * define custom API documentation with full control over request/response schemas.
7
+ *
8
+ * @packageDocumentation
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import {createOpenApiBuilder} from "./openApiBuilder";
13
+ *
14
+ * // Create middleware with custom documentation
15
+ * const middleware = createOpenApiBuilder(options)
16
+ * .withTags(["users"])
17
+ * .withSummary("Get user statistics")
18
+ * .withDescription("Returns aggregated statistics for the current user")
19
+ * .withQueryParameter("period", {type: "string"}, {
20
+ * description: "Time period for statistics",
21
+ * required: false,
22
+ * })
23
+ * .withResponse<{count: number; average: number}>(200, {
24
+ * count: {type: "number", description: "Total count"},
25
+ * average: {type: "number", description: "Average value"},
26
+ * })
27
+ * .build();
28
+ *
29
+ * router.get("/stats", middleware, statsHandler);
30
+ * ```
31
+ */
32
+ import merge from "lodash/merge";
33
+
34
+ import type {modelRouterOptions} from "./api";
35
+ import {logger} from "./logger";
36
+ import {defaultOpenApiErrorResponses} from "./openApi";
37
+
38
+ /**
39
+ * Defines a property within an OpenAPI schema.
40
+ *
41
+ * This type represents the structure of individual properties in request bodies,
42
+ * response objects, and nested schemas. It supports primitive types, arrays,
43
+ * nested objects, and additional properties for map-like structures.
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * // Simple string property
48
+ * const nameProperty: OpenApiSchemaProperty = {
49
+ * type: "string",
50
+ * description: "User's full name",
51
+ * };
52
+ *
53
+ * // Array of objects
54
+ * const itemsProperty: OpenApiSchemaProperty = {
55
+ * type: "array",
56
+ * items: {
57
+ * type: "object",
58
+ * properties: {
59
+ * id: {type: "string"},
60
+ * value: {type: "number"},
61
+ * },
62
+ * },
63
+ * };
64
+ *
65
+ * // Object with additional properties (map/dictionary)
66
+ * const metadataProperty: OpenApiSchemaProperty = {
67
+ * type: "object",
68
+ * additionalProperties: {type: "string"},
69
+ * };
70
+ * ```
71
+ */
72
+ export type OpenApiSchemaProperty = {
73
+ /** The JSON Schema type (e.g., "string", "number", "boolean", "object", "array") */
74
+ type: string;
75
+ /** Human-readable description of the property */
76
+ description?: string;
77
+ /** Format hint for the type (e.g., "date-time", "email", "uri", "uuid") */
78
+ format?: string;
79
+ /** Schema for array items when type is "array" */
80
+ items?: OpenApiSchemaProperty;
81
+ /** Nested properties when type is "object" */
82
+ properties?: Record<string, OpenApiSchemaProperty>;
83
+ /** Schema for additional properties or boolean to allow/disallow them */
84
+ additionalProperties?: OpenApiSchemaProperty | boolean;
85
+ /** Whether this property is required in the parent object */
86
+ required?: boolean;
87
+ };
88
+
89
+ /**
90
+ * Defines the top-level schema for request bodies and responses.
91
+ *
92
+ * This type represents complete object schemas used in OpenAPI operations,
93
+ * typically for request bodies and response content.
94
+ *
95
+ * @example
96
+ * ```typescript
97
+ * const userSchema: OpenApiSchema = {
98
+ * type: "object",
99
+ * properties: {
100
+ * id: {type: "string"},
101
+ * name: {type: "string"},
102
+ * email: {type: "string", format: "email"},
103
+ * },
104
+ * required: ["id", "name"],
105
+ * };
106
+ * ```
107
+ */
108
+ export type OpenApiSchema = {
109
+ /** The JSON Schema type (typically "object" or "array") */
110
+ type: string;
111
+ /** Property definitions for object types */
112
+ properties?: Record<string, OpenApiSchemaProperty>;
113
+ /** List of required property names */
114
+ required?: string[];
115
+ /** Schema for array items when type is "array" */
116
+ items?: OpenApiSchemaProperty;
117
+ /** Schema for additional properties or boolean to allow/disallow them */
118
+ additionalProperties?: OpenApiSchemaProperty | boolean;
119
+ };
120
+
121
+ /**
122
+ * Defines a parameter in an OpenAPI operation.
123
+ *
124
+ * Parameters can be passed via query string, path segments, or headers.
125
+ * Path parameters are always required by OpenAPI specification.
126
+ *
127
+ * @example
128
+ * ```typescript
129
+ * // Query parameter
130
+ * const limitParam: OpenApiParameter = {
131
+ * in: "query",
132
+ * name: "limit",
133
+ * required: false,
134
+ * schema: {type: "number"},
135
+ * description: "Maximum number of results",
136
+ * };
137
+ *
138
+ * // Path parameter
139
+ * const idParam: OpenApiParameter = {
140
+ * in: "path",
141
+ * name: "id",
142
+ * required: true,
143
+ * schema: {type: "string"},
144
+ * description: "Resource identifier",
145
+ * };
146
+ * ```
147
+ */
148
+ export type OpenApiParameter = {
149
+ /** Location of the parameter */
150
+ in: "query" | "path" | "header";
151
+ /** Name of the parameter */
152
+ name: string;
153
+ /** Whether the parameter is required (path params are always required) */
154
+ required?: boolean;
155
+ /** Schema defining the parameter's type and format */
156
+ schema: OpenApiSchemaProperty;
157
+ /** Human-readable description of the parameter */
158
+ description?: string;
159
+ };
160
+
161
+ /**
162
+ * Defines a response in an OpenAPI operation.
163
+ *
164
+ * Responses include a description and optionally content with a schema
165
+ * for the response body.
166
+ *
167
+ * @example
168
+ * ```typescript
169
+ * const successResponse: OpenApiResponse = {
170
+ * description: "Successfully retrieved user",
171
+ * content: {
172
+ * "application/json": {
173
+ * schema: {
174
+ * type: "object",
175
+ * properties: {
176
+ * id: {type: "string"},
177
+ * name: {type: "string"},
178
+ * },
179
+ * },
180
+ * },
181
+ * },
182
+ * };
183
+ * ```
184
+ */
185
+ export type OpenApiResponse = {
186
+ /** Human-readable description of the response */
187
+ description: string;
188
+ /** Content definitions keyed by media type */
189
+ content?: {
190
+ [mediaType: string]: {
191
+ schema: OpenApiSchema;
192
+ };
193
+ };
194
+ };
195
+
196
+ /**
197
+ * Internal configuration object for the OpenAPI middleware builder.
198
+ *
199
+ * This interface represents the accumulated configuration from builder method calls.
200
+ */
201
+ interface OpenApiConfig {
202
+ /** Tags for grouping operations in API documentation */
203
+ tags?: string[];
204
+ /** Short summary of the operation */
205
+ summary?: string;
206
+ /** Detailed description of the operation */
207
+ description?: string;
208
+ /** Operation parameters (query, path, header) */
209
+ parameters?: OpenApiParameter[];
210
+ /** Request body configuration */
211
+ requestBody?: {
212
+ /** Whether the request body is required */
213
+ required?: boolean;
214
+ /** Content definitions keyed by media type */
215
+ content: {
216
+ [mediaType: string]: {
217
+ schema: OpenApiSchema;
218
+ };
219
+ };
220
+ };
221
+ /** Response definitions keyed by status code */
222
+ responses: Record<number | string, OpenApiResponse>;
223
+ }
224
+
225
+ /**
226
+ * A fluent builder for constructing OpenAPI middleware.
227
+ *
228
+ * This class provides a chainable API for defining OpenAPI documentation
229
+ * for Express routes. It supports defining tags, summaries, descriptions,
230
+ * request bodies, responses, and parameters.
231
+ *
232
+ * The builder pattern allows for flexible, readable configuration that
233
+ * produces middleware compatible with the express-openapi-validator library.
234
+ *
235
+ * @example
236
+ * ```typescript
237
+ * const middleware = new OpenApiMiddlewareBuilder(options)
238
+ * .withTags(["users"])
239
+ * .withSummary("Create a new user")
240
+ * .withRequestBody<{name: string; email: string}>({
241
+ * name: {type: "string", required: true},
242
+ * email: {type: "string", format: "email", required: true},
243
+ * })
244
+ * .withResponse<{id: string; name: string}>(201, {
245
+ * id: {type: "string"},
246
+ * name: {type: "string"},
247
+ * })
248
+ * .build();
249
+ * ```
250
+ */
251
+ export class OpenApiMiddlewareBuilder {
252
+ /** Router options containing OpenAPI configuration */
253
+ private options: Partial<modelRouterOptions<any>>;
254
+
255
+ /** Accumulated OpenAPI configuration from builder methods */
256
+ private config: OpenApiConfig;
257
+
258
+ /**
259
+ * Creates a new OpenApiMiddlewareBuilder instance.
260
+ *
261
+ * @param options - Router options containing the OpenAPI path configuration
262
+ */
263
+ constructor(options: Partial<modelRouterOptions<any>>) {
264
+ this.options = options;
265
+ this.config = {
266
+ responses: {},
267
+ };
268
+ }
269
+
270
+ /**
271
+ * Sets the tags for the OpenAPI operation.
272
+ *
273
+ * Tags are used to group operations in the API documentation.
274
+ *
275
+ * @param tags - Array of tag names
276
+ * @returns The builder instance for chaining
277
+ *
278
+ * @example
279
+ * ```typescript
280
+ * builder.withTags(["users", "authentication"]);
281
+ * ```
282
+ */
283
+ withTags(tags: string[]): this {
284
+ this.config.tags = tags;
285
+ return this;
286
+ }
287
+
288
+ /**
289
+ * Sets the summary for the OpenAPI operation.
290
+ *
291
+ * The summary is a brief description shown in API documentation listings.
292
+ *
293
+ * @param summary - Short description of the operation
294
+ * @returns The builder instance for chaining
295
+ *
296
+ * @example
297
+ * ```typescript
298
+ * builder.withSummary("Get user by ID");
299
+ * ```
300
+ */
301
+ withSummary(summary: string): this {
302
+ this.config.summary = summary;
303
+ return this;
304
+ }
305
+
306
+ /**
307
+ * Sets the description for the OpenAPI operation.
308
+ *
309
+ * The description provides detailed information about the operation,
310
+ * including usage notes, examples, and caveats.
311
+ *
312
+ * @param description - Detailed description of the operation
313
+ * @returns The builder instance for chaining
314
+ *
315
+ * @example
316
+ * ```typescript
317
+ * builder.withDescription("Retrieves a user by their unique identifier. Returns 404 if not found.");
318
+ * ```
319
+ */
320
+ withDescription(description: string): this {
321
+ this.config.description = description;
322
+ return this;
323
+ }
324
+
325
+ /**
326
+ * Sets the request body schema for the OpenAPI operation.
327
+ *
328
+ * Properties marked with `required: true` will be added to the schema's
329
+ * required array automatically.
330
+ *
331
+ * @typeParam T - Type representing the request body structure
332
+ * @param schema - Object mapping property names to their OpenAPI schema definitions
333
+ * @param options - Optional configuration for the request body
334
+ * @param options.required - Whether the request body itself is required (default: true)
335
+ * @param options.mediaType - Media type for the request body (default: "application/json")
336
+ * @returns The builder instance for chaining
337
+ *
338
+ * @example
339
+ * ```typescript
340
+ * builder.withRequestBody<{name: string; age: number}>({
341
+ * name: {type: "string", description: "User name", required: true},
342
+ * age: {type: "number", description: "User age"},
343
+ * });
344
+ * ```
345
+ */
346
+ withRequestBody<T extends Record<string, any>>(
347
+ schema: {
348
+ [K in keyof T]: OpenApiSchemaProperty;
349
+ },
350
+ options?: {
351
+ required?: boolean;
352
+ mediaType?: string;
353
+ }
354
+ ): this {
355
+ const required = Object.entries(schema)
356
+ .filter(([_, prop]) => prop.required)
357
+ .map(([key, _]) => key);
358
+
359
+ this.config.requestBody = {
360
+ content: {
361
+ [options?.mediaType ?? "application/json"]: {
362
+ schema: {
363
+ properties: schema,
364
+ required: required.length > 0 ? required : undefined,
365
+ type: "object",
366
+ },
367
+ },
368
+ },
369
+ required: options?.required ?? true,
370
+ };
371
+ return this;
372
+ }
373
+
374
+ /**
375
+ * Adds a response definition to the OpenAPI operation.
376
+ *
377
+ * Can accept either an object schema or a simple string description
378
+ * for responses without a body (e.g., 204 No Content).
379
+ *
380
+ * @typeParam T - Type representing the response body structure
381
+ * @param statusCode - HTTP status code for this response
382
+ * @param schema - Either an object schema or a description string
383
+ * @param options - Optional configuration for the response
384
+ * @param options.description - Description of the response (default: "Success")
385
+ * @param options.mediaType - Media type for the response (default: "application/json")
386
+ * @returns The builder instance for chaining
387
+ *
388
+ * @example
389
+ * ```typescript
390
+ * // Response with body
391
+ * builder.withResponse<{id: string}>(200, {
392
+ * id: {type: "string", description: "Created resource ID"},
393
+ * }, {description: "Resource created successfully"});
394
+ *
395
+ * // Response without body
396
+ * builder.withResponse(204, "No content");
397
+ * ```
398
+ */
399
+ withResponse<T extends Record<string, any>>(
400
+ statusCode: number,
401
+ schema:
402
+ | {
403
+ [K in keyof T]: OpenApiSchemaProperty;
404
+ }
405
+ | string,
406
+ options?: {
407
+ description?: string;
408
+ mediaType?: string;
409
+ }
410
+ ): this {
411
+ if (typeof schema === "string") {
412
+ this.config.responses[statusCode] = {
413
+ description: schema,
414
+ };
415
+ } else {
416
+ this.config.responses[statusCode] = {
417
+ content: {
418
+ [options?.mediaType ?? "application/json"]: {
419
+ schema: {
420
+ properties: schema,
421
+ type: "object",
422
+ },
423
+ },
424
+ },
425
+ description: options?.description ?? "Success",
426
+ };
427
+ }
428
+ return this;
429
+ }
430
+
431
+ /**
432
+ * Adds an array response definition to the OpenAPI operation.
433
+ *
434
+ * Use this method when the response is an array of objects rather
435
+ * than a single object.
436
+ *
437
+ * @typeParam T - Type representing the structure of each array item
438
+ * @param statusCode - HTTP status code for this response
439
+ * @param itemSchema - Schema for each item in the response array
440
+ * @param options - Optional configuration for the response
441
+ * @param options.description - Description of the response (default: "Success")
442
+ * @param options.mediaType - Media type for the response (default: "application/json")
443
+ * @returns The builder instance for chaining
444
+ *
445
+ * @example
446
+ * ```typescript
447
+ * builder.withArrayResponse<{id: string; name: string}>(200, {
448
+ * id: {type: "string"},
449
+ * name: {type: "string"},
450
+ * }, {description: "List of users"});
451
+ * ```
452
+ */
453
+ withArrayResponse<T extends Record<string, any>>(
454
+ statusCode: number,
455
+ itemSchema: {
456
+ [K in keyof T]: OpenApiSchemaProperty;
457
+ },
458
+ options?: {
459
+ description?: string;
460
+ mediaType?: string;
461
+ }
462
+ ): this {
463
+ this.config.responses[statusCode] = {
464
+ content: {
465
+ [options?.mediaType ?? "application/json"]: {
466
+ schema: {
467
+ items: {
468
+ properties: itemSchema,
469
+ type: "object",
470
+ },
471
+ type: "array",
472
+ },
473
+ },
474
+ },
475
+ description: options?.description ?? "Success",
476
+ };
477
+ return this;
478
+ }
479
+
480
+ /**
481
+ * Adds a query parameter to the OpenAPI operation.
482
+ *
483
+ * Query parameters are passed in the URL query string (e.g., `?limit=10`).
484
+ *
485
+ * @param name - Name of the query parameter
486
+ * @param schema - Schema defining the parameter's type and format
487
+ * @param options - Optional configuration for the parameter
488
+ * @param options.required - Whether the parameter is required (default: false)
489
+ * @param options.description - Human-readable description of the parameter
490
+ * @returns The builder instance for chaining
491
+ *
492
+ * @example
493
+ * ```typescript
494
+ * builder.withQueryParameter("limit", {type: "number"}, {
495
+ * required: false,
496
+ * description: "Maximum number of results to return",
497
+ * });
498
+ * ```
499
+ */
500
+ withQueryParameter(
501
+ name: string,
502
+ schema: OpenApiSchemaProperty,
503
+ options?: {
504
+ required?: boolean;
505
+ description?: string;
506
+ }
507
+ ): this {
508
+ if (!this.config.parameters) {
509
+ this.config.parameters = [];
510
+ }
511
+ this.config.parameters.push({
512
+ description: options?.description,
513
+ in: "query",
514
+ name,
515
+ required: options?.required ?? false,
516
+ schema,
517
+ });
518
+ return this;
519
+ }
520
+
521
+ /**
522
+ * Adds a path parameter to the OpenAPI operation.
523
+ *
524
+ * Path parameters are embedded in the URL path (e.g., `/users/:id`).
525
+ * Path parameters are always required per OpenAPI specification.
526
+ *
527
+ * @param name - Name of the path parameter (must match the route parameter)
528
+ * @param schema - Schema defining the parameter's type and format
529
+ * @param options - Optional configuration for the parameter
530
+ * @param options.description - Human-readable description of the parameter
531
+ * @returns The builder instance for chaining
532
+ *
533
+ * @example
534
+ * ```typescript
535
+ * builder.withPathParameter("id", {type: "string", format: "uuid"}, {
536
+ * description: "Unique identifier of the user",
537
+ * });
538
+ * ```
539
+ */
540
+ withPathParameter(
541
+ name: string,
542
+ schema: OpenApiSchemaProperty,
543
+ options?: {
544
+ description?: string;
545
+ }
546
+ ): this {
547
+ if (!this.config.parameters) {
548
+ this.config.parameters = [];
549
+ }
550
+ this.config.parameters.push({
551
+ description: options?.description,
552
+ in: "path",
553
+ name,
554
+ required: true,
555
+ schema,
556
+ });
557
+ return this;
558
+ }
559
+
560
+ /**
561
+ * Builds and returns the OpenAPI middleware.
562
+ *
563
+ * This method finalizes the configuration and returns Express middleware
564
+ * that integrates with the OpenAPI documentation system. If no OpenAPI
565
+ * path is configured in options, returns a no-op middleware.
566
+ *
567
+ * Default error responses (400, 401, 403, 404, 405) are automatically
568
+ * merged with the configured responses.
569
+ *
570
+ * @returns Express middleware function for OpenAPI documentation
571
+ *
572
+ * @example
573
+ * ```typescript
574
+ * const middleware = builder
575
+ * .withTags(["users"])
576
+ * .withResponse(200, {id: {type: "string"}})
577
+ * .build();
578
+ *
579
+ * router.get("/users/:id", middleware, getUserHandler);
580
+ * ```
581
+ */
582
+ build(): any {
583
+ const noop = (_a: any, _b: any, next: () => void): void => next();
584
+
585
+ if (!this.options.openApi?.path) {
586
+ logger.debug("No options.openApi provided, skipping OpenApiMiddleware");
587
+ return noop;
588
+ }
589
+
590
+ return this.options.openApi.path(
591
+ merge(
592
+ {
593
+ ...this.config,
594
+ responses: {
595
+ ...this.config.responses,
596
+ ...defaultOpenApiErrorResponses,
597
+ },
598
+ },
599
+ this.options.openApiOverwrite?.get ?? {}
600
+ )
601
+ );
602
+ }
603
+ }
604
+
605
+ /**
606
+ * Creates a new OpenAPI middleware builder.
607
+ *
608
+ * This is the recommended entry point for creating custom OpenAPI middleware.
609
+ * It returns a fluent builder that allows you to chain configuration methods.
610
+ *
611
+ * @param options - Router options containing the OpenAPI configuration
612
+ * @returns A new OpenApiMiddlewareBuilder instance
613
+ *
614
+ * @example
615
+ * ```typescript
616
+ * import {createOpenApiBuilder} from "./openApiBuilder";
617
+ *
618
+ * const statsMiddleware = createOpenApiBuilder(options)
619
+ * .withTags(["analytics"])
620
+ * .withSummary("Get usage statistics")
621
+ * .withQueryParameter("startDate", {type: "string", format: "date"})
622
+ * .withQueryParameter("endDate", {type: "string", format: "date"})
623
+ * .withResponse<{totalUsers: number; activeUsers: number}>(200, {
624
+ * totalUsers: {type: "number", description: "Total registered users"},
625
+ * activeUsers: {type: "number", description: "Users active in period"},
626
+ * })
627
+ * .build();
628
+ *
629
+ * router.get("/analytics/stats", statsMiddleware, getStatsHandler);
630
+ * ```
631
+ */
632
+ export function createOpenApiBuilder(
633
+ options: Partial<modelRouterOptions<any>>
634
+ ): OpenApiMiddlewareBuilder {
635
+ return new OpenApiMiddlewareBuilder(options);
636
+ }
@@ -0,0 +1,40 @@
1
+ import crypto from "node:crypto";
2
+ import type {NextFunction, Request, Response} from "express";
3
+
4
+ /**
5
+ * Middleware to add ETag support for OpenAPI JSON endpoint.
6
+ * This middleware should be added before the @wesleytodd/openapi middleware
7
+ * to intercept requests to /openapi.json and add conditional request support.
8
+ */
9
+ export function openApiEtagMiddleware(req: Request, res: Response, next: NextFunction): void {
10
+ // Only handle GET requests to /openapi.json
11
+ if (req.method !== "GET" || req.path !== "/openapi.json") {
12
+ next();
13
+ return;
14
+ }
15
+
16
+ // Store original res.json to intercept the response
17
+ const originalJson = res.json.bind(res);
18
+
19
+ res.json = (body: any) => {
20
+ // Generate ETag based on the JSON content
21
+ const jsonString = JSON.stringify(body);
22
+ const etag = `"${crypto.createHash("sha256").update(jsonString).digest("hex").substring(0, 16)}"`;
23
+
24
+ // Set ETag header
25
+ res.set("ETag", etag);
26
+
27
+ // Check If-None-Match header for conditional requests
28
+ const ifNoneMatch = req.get("If-None-Match");
29
+ if (ifNoneMatch === etag) {
30
+ // Resource hasn't changed, return 304 Not Modified
31
+ res.status(304).end();
32
+ return res;
33
+ }
34
+
35
+ // Resource has changed or no conditional header, return the content
36
+ return originalJson(body);
37
+ };
38
+
39
+ next();
40
+ }