@rabstack/rab-api 1.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.
package/index.cjs.js ADDED
@@ -0,0 +1,1105 @@
1
+ 'use strict';
2
+
3
+ require('reflect-metadata');
4
+ var express = require('express');
5
+ var typedi = require('typedi');
6
+ var composeMiddleware = require('compose-middleware');
7
+ var jwt = require('jsonwebtoken');
8
+
9
+ function _extends() {
10
+ _extends = Object.assign || function assign(target) {
11
+ for(var i = 1; i < arguments.length; i++){
12
+ var source = arguments[i];
13
+ for(var key in source)if (Object.prototype.hasOwnProperty.call(source, key)) target[key] = source[key];
14
+ }
15
+ return target;
16
+ };
17
+ return _extends.apply(this, arguments);
18
+ }
19
+
20
+ function _object_without_properties_loose(source, excluded) {
21
+ if (source == null) return {};
22
+ var target = {};
23
+ var sourceKeys = Object.keys(source);
24
+ var key, i;
25
+ for(i = 0; i < sourceKeys.length; i++){
26
+ key = sourceKeys[i];
27
+ if (excluded.indexOf(key) >= 0) continue;
28
+ target[key] = source[key];
29
+ }
30
+ return target;
31
+ }
32
+
33
+ /**
34
+ * Controller decorator - marks a class as an injectable controller.
35
+ * Alias for TypeDI's @Service decorator.
36
+ */ const Controller = typedi.Service;
37
+ /**
38
+ * Injectable decorator - marks a class as injectable via dependency injection.
39
+ * Alias for TypeDI's @Service decorator.
40
+ */ const Injectable = typedi.Service;
41
+ /**
42
+ * Dependency injection container.
43
+ * Provides access to TypeDI's Container for manual service retrieval.
44
+ */ const DiContainer = typedi.Container;
45
+ /**
46
+ * Metadata key used to store route information via reflect-metadata.
47
+ */ const CONTROLLER_ROUTE_KEY = 'controller:route';
48
+ /**
49
+ * Core route decorator factory.
50
+ * Creates route decorators for different HTTP methods.
51
+ *
52
+ * @param method - HTTP method for this route
53
+ * @param path - URL path pattern (supports Express-style params like ':id')
54
+ * @param options - Route configuration options
55
+ * @returns Class decorator function
56
+ */ function AtomRoute(method, path, options) {
57
+ return (target)=>{
58
+ typedi.Service()(target);
59
+ const routeMetadata = {
60
+ method,
61
+ path,
62
+ handlerName: 'handler',
63
+ options
64
+ };
65
+ if (!typedi.Container.has(target)) {
66
+ typedi.Container.set({
67
+ id: target,
68
+ type: target
69
+ });
70
+ }
71
+ Reflect.defineMetadata(CONTROLLER_ROUTE_KEY, routeMetadata, target);
72
+ };
73
+ }
74
+ /**
75
+ * POST route decorator.
76
+ * Registers a controller class as a POST endpoint handler.
77
+ *
78
+ * @param path - URL path pattern (e.g., '/users' or '/users/:id')
79
+ * @param options - Route configuration options (validation, permissions, etc.)
80
+ * @returns Class decorator
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * @Post('/users', {
85
+ * bodySchema: createUserSchema,
86
+ * permission: 'canCreateUser',
87
+ * })
88
+ * export class CreateUserController implements AtomApiPost<T> {
89
+ * execute = async (request) => {
90
+ * return this.userService.create(request.body);
91
+ * };
92
+ * }
93
+ * ```
94
+ */ function Post(path, options) {
95
+ return AtomRoute('post', path, options);
96
+ }
97
+ /**
98
+ * GET route decorator.
99
+ * Registers a controller class as a GET endpoint handler.
100
+ *
101
+ * @param path - URL path pattern (e.g., '/users' or '/users/:id')
102
+ * @param options - Route configuration options (validation, permissions, etc.)
103
+ * @returns Class decorator
104
+ *
105
+ * @example
106
+ * ```typescript
107
+ * @Get('/users/:id', {
108
+ * permission: 'canReadUser',
109
+ * })
110
+ * export class GetUserController implements AtomApiGet<T> {
111
+ * execute = async (request) => {
112
+ * return this.userService.findById(request.params.id);
113
+ * };
114
+ * }
115
+ * ```
116
+ */ function Get(path, options) {
117
+ return AtomRoute('get', path, options);
118
+ }
119
+ /**
120
+ * DELETE route decorator.
121
+ * Registers a controller class as a DELETE endpoint handler.
122
+ *
123
+ * @param path - URL path pattern (e.g., '/users/:id')
124
+ * @param options - Route configuration options (validation, permissions, etc.)
125
+ * @returns Class decorator
126
+ *
127
+ * @example
128
+ * ```typescript
129
+ * @Delete('/users/:id', {
130
+ * permission: 'canDeleteUser',
131
+ * })
132
+ * export class DeleteUserController implements AtomApiDelete<T> {
133
+ * execute = async (request) => {
134
+ * await this.userService.delete(request.params.id);
135
+ * return { success: true };
136
+ * };
137
+ * }
138
+ * ```
139
+ */ function Delete(path, options) {
140
+ return AtomRoute('delete', path, options);
141
+ }
142
+ /**
143
+ * PUT route decorator.
144
+ * Registers a controller class as a PUT endpoint handler.
145
+ * Use for full resource replacement.
146
+ *
147
+ * @param path - URL path pattern (e.g., '/users/:id')
148
+ * @param options - Route configuration options (validation, permissions, etc.)
149
+ * @returns Class decorator
150
+ *
151
+ * @example
152
+ * ```typescript
153
+ * @Put('/users/:id', {
154
+ * bodySchema: updateUserSchema,
155
+ * permission: 'canUpdateUser',
156
+ * })
157
+ * export class UpdateUserController implements AtomApiPut<T> {
158
+ * execute = async (request) => {
159
+ * return this.userService.update(request.params.id, request.body);
160
+ * };
161
+ * }
162
+ * ```
163
+ */ function Put(path, options) {
164
+ return AtomRoute('put', path, options);
165
+ }
166
+ /**
167
+ * PATCH route decorator.
168
+ * Registers a controller class as a PATCH endpoint handler.
169
+ * Use for partial resource updates.
170
+ *
171
+ * @param path - URL path pattern (e.g., '/users/:id')
172
+ * @param options - Route configuration options (validation, permissions, etc.)
173
+ * @returns Class decorator
174
+ *
175
+ * @example
176
+ * ```typescript
177
+ * @Patch('/users/:id', {
178
+ * bodySchema: patchUserSchema,
179
+ * permission: 'canUpdateUser',
180
+ * })
181
+ * export class PatchUserController implements AtomApiPatch<T> {
182
+ * execute = async (request) => {
183
+ * return this.userService.patch(request.params.id, request.body);
184
+ * };
185
+ * }
186
+ * ```
187
+ */ function Patch(path, options) {
188
+ return AtomRoute('patch', path, options);
189
+ }
190
+
191
+ const isCallBackPipe = (pipe)=>typeof pipe === 'function' && pipe.length == 1;
192
+
193
+ /**
194
+ * Base class for all RabAPI errors.
195
+ * Extends the native Error class with additional HTTP context.
196
+ */ class RabApiError extends Error {
197
+ /**
198
+ * @param message - Human-readable error message
199
+ * @param statusCode - HTTP status code
200
+ * @param errorCode - Optional machine-readable error code
201
+ * @param errors - Optional array of detailed error messages
202
+ */ constructor(message, statusCode, errorCode, errors){
203
+ super(message);
204
+ this.errors = errors;
205
+ this.message = message;
206
+ this.errorCode = errorCode;
207
+ this.statusCode = statusCode;
208
+ this.name = 'RabApiError';
209
+ Error.captureStackTrace(this, this.constructor);
210
+ }
211
+ }
212
+ /**
213
+ * Represents a 400 Bad Request error.
214
+ * Use this for client-side validation failures or malformed requests.
215
+ *
216
+ * @example
217
+ * ```typescript
218
+ * throw new BadRequestException(
219
+ * 'Invalid email format',
220
+ * ['Email must be a valid email address'],
221
+ * 'INVALID_EMAIL'
222
+ * );
223
+ * ```
224
+ */ class BadRequestException extends RabApiError {
225
+ /**
226
+ * @param message - Human-readable error message
227
+ * @param errors - Optional array of detailed validation errors
228
+ * @param errorCode - Optional machine-readable error code for client handling
229
+ */ constructor(message, errors, errorCode){
230
+ super(message, 400, errorCode, errors);
231
+ this.name = 'BadRequestException';
232
+ Error.captureStackTrace(this, this.constructor);
233
+ }
234
+ }
235
+ /**
236
+ * Represents a 401 Unauthorized error.
237
+ * Use this when authentication is required but missing or invalid.
238
+ *
239
+ * @example
240
+ * ```typescript
241
+ * throw new UnauthorizedException('Invalid token', 'TOKEN_EXPIRED');
242
+ * ```
243
+ */ class UnauthorizedException extends RabApiError {
244
+ /**
245
+ * @param message - Human-readable error message (default: 'Unauthorized')
246
+ * @param errorCode - Optional machine-readable error code
247
+ */ constructor(message = 'Unauthorized', errorCode){
248
+ super(message, 401, errorCode);
249
+ this.name = 'UnauthorizedException';
250
+ Error.captureStackTrace(this, this.constructor);
251
+ }
252
+ }
253
+ /**
254
+ * Represents a 403 Forbidden error.
255
+ * Use this when the user is authenticated but lacks permission.
256
+ *
257
+ * @example
258
+ * ```typescript
259
+ * throw new ForbiddenException('You do not have permission to access this resource', 'INSUFFICIENT_PERMISSIONS');
260
+ * ```
261
+ */ class ForbiddenException extends RabApiError {
262
+ /**
263
+ * @param message - Human-readable error message (default: 'Forbidden')
264
+ * @param errorCode - Optional machine-readable error code
265
+ */ constructor(message = 'Forbidden', errorCode){
266
+ super(message, 403, errorCode);
267
+ this.name = 'ForbiddenException';
268
+ Error.captureStackTrace(this, this.constructor);
269
+ }
270
+ }
271
+ /**
272
+ * Represents a 404 Not Found error.
273
+ * Use this when a requested resource does not exist.
274
+ *
275
+ * @example
276
+ * ```typescript
277
+ * throw new NotFoundException('User not found', 'USER_NOT_FOUND');
278
+ * ```
279
+ */ class NotFoundException extends RabApiError {
280
+ /**
281
+ * @param message - Human-readable error message (default: 'Not Found')
282
+ * @param errorCode - Optional machine-readable error code
283
+ */ constructor(message = 'Not Found', errorCode){
284
+ super(message, 404, errorCode);
285
+ this.name = 'NotFoundException';
286
+ Error.captureStackTrace(this, this.constructor);
287
+ }
288
+ }
289
+ /**
290
+ * Represents a 405 Method Not Allowed error.
291
+ * Use this when an HTTP method is not supported for a resource.
292
+ *
293
+ * @example
294
+ * ```typescript
295
+ * throw new MethodNotAllowedException('DELETE method not allowed on this resource');
296
+ * ```
297
+ */ class MethodNotAllowedException extends RabApiError {
298
+ /**
299
+ * @param message - Human-readable error message (default: 'Method Not Allowed')
300
+ * @param errorCode - Optional machine-readable error code
301
+ */ constructor(message = 'Method Not Allowed', errorCode){
302
+ super(message, 405, errorCode);
303
+ this.name = 'MethodNotAllowedException';
304
+ Error.captureStackTrace(this, this.constructor);
305
+ }
306
+ }
307
+ /**
308
+ * Represents a 408 Request Timeout error.
309
+ * Use this when the server times out waiting for a request.
310
+ *
311
+ * @example
312
+ * ```typescript
313
+ * throw new RequestTimeoutException('Request took too long to complete', 'REQUEST_TIMEOUT');
314
+ * ```
315
+ */ class RequestTimeoutException extends RabApiError {
316
+ /**
317
+ * @param message - Human-readable error message (default: 'Request Timeout')
318
+ * @param errorCode - Optional machine-readable error code
319
+ */ constructor(message = 'Request Timeout', errorCode){
320
+ super(message, 408, errorCode);
321
+ this.name = 'RequestTimeoutException';
322
+ Error.captureStackTrace(this, this.constructor);
323
+ }
324
+ }
325
+ /**
326
+ * Represents a 409 Conflict error.
327
+ * Use this when a request conflicts with the current state (e.g., duplicate resources).
328
+ *
329
+ * @example
330
+ * ```typescript
331
+ * throw new ConflictException('Email already exists', 'EMAIL_CONFLICT');
332
+ * ```
333
+ */ class ConflictException extends RabApiError {
334
+ /**
335
+ * @param message - Human-readable error message
336
+ * @param errorCode - Optional machine-readable error code
337
+ */ constructor(message, errorCode){
338
+ super(message, 409, errorCode);
339
+ this.name = 'ConflictException';
340
+ Error.captureStackTrace(this, this.constructor);
341
+ }
342
+ }
343
+ /**
344
+ * Represents a 413 Payload Too Large error.
345
+ * Use this when the request payload exceeds size limits.
346
+ *
347
+ * @example
348
+ * ```typescript
349
+ * throw new PayloadTooLargeException('File size exceeds 10MB limit', 'FILE_TOO_LARGE');
350
+ * ```
351
+ */ class PayloadTooLargeException extends RabApiError {
352
+ /**
353
+ * @param message - Human-readable error message (default: 'Payload Too Large')
354
+ * @param errorCode - Optional machine-readable error code
355
+ */ constructor(message = 'Payload Too Large', errorCode){
356
+ super(message, 413, errorCode);
357
+ this.name = 'PayloadTooLargeException';
358
+ Error.captureStackTrace(this, this.constructor);
359
+ }
360
+ }
361
+ /**
362
+ * Represents a 422 Unprocessable Entity error.
363
+ * Use this for semantic validation errors (valid syntax but invalid semantics).
364
+ *
365
+ * @example
366
+ * ```typescript
367
+ * throw new UnprocessableEntityException(
368
+ * 'Validation failed',
369
+ * ['Age must be at least 18', 'Email domain not allowed'],
370
+ * 'VALIDATION_FAILED'
371
+ * );
372
+ * ```
373
+ */ class UnprocessableEntityException extends RabApiError {
374
+ /**
375
+ * @param message - Human-readable error message (default: 'Unprocessable Entity')
376
+ * @param errors - Optional array of detailed validation errors
377
+ * @param errorCode - Optional machine-readable error code
378
+ */ constructor(message = 'Unprocessable Entity', errors, errorCode){
379
+ super(message, 422, errorCode, errors);
380
+ this.name = 'UnprocessableEntityException';
381
+ Error.captureStackTrace(this, this.constructor);
382
+ }
383
+ }
384
+ /**
385
+ * Represents a 429 Too Many Requests error.
386
+ * Use this when rate limiting is exceeded.
387
+ *
388
+ * @example
389
+ * ```typescript
390
+ * throw new TooManyRequestsException('Rate limit exceeded. Try again in 60 seconds', 'RATE_LIMIT_EXCEEDED');
391
+ * ```
392
+ */ class TooManyRequestsException extends RabApiError {
393
+ /**
394
+ * @param message - Human-readable error message (default: 'Too Many Requests')
395
+ * @param errorCode - Optional machine-readable error code
396
+ */ constructor(message = 'Too Many Requests', errorCode){
397
+ super(message, 429, errorCode);
398
+ this.name = 'TooManyRequestsException';
399
+ Error.captureStackTrace(this, this.constructor);
400
+ }
401
+ }
402
+ /**
403
+ * Represents a 500 Internal Server Error.
404
+ * Use this for unexpected server-side errors.
405
+ *
406
+ * @example
407
+ * ```typescript
408
+ * throw new InternalServerErrorException('Database connection failed', 'DB_ERROR');
409
+ * ```
410
+ */ class InternalServerErrorException extends RabApiError {
411
+ /**
412
+ * @param message - Human-readable error message (default: 'Internal Server Error')
413
+ * @param errorCode - Optional machine-readable error code
414
+ */ constructor(message = 'Internal Server Error', errorCode){
415
+ super(message, 500, errorCode);
416
+ this.name = 'InternalServerErrorException';
417
+ Error.captureStackTrace(this, this.constructor);
418
+ }
419
+ }
420
+ /**
421
+ * Represents a 503 Service Unavailable error.
422
+ * Use this when the server is temporarily unable to handle requests.
423
+ *
424
+ * @example
425
+ * ```typescript
426
+ * throw new ServiceUnavailableException('System maintenance in progress', 'MAINTENANCE');
427
+ * ```
428
+ */ class ServiceUnavailableException extends RabApiError {
429
+ /**
430
+ * @param message - Human-readable error message (default: 'Service Unavailable')
431
+ * @param errorCode - Optional machine-readable error code
432
+ */ constructor(message = 'Service Unavailable', errorCode){
433
+ super(message, 503, errorCode);
434
+ this.name = 'ServiceUnavailableException';
435
+ Error.captureStackTrace(this, this.constructor);
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Global error handler middleware for Express.
441
+ * Handles RabApiError instances with proper status codes and error formatting.
442
+ * Falls back to 500 Internal Server Error for unexpected errors.
443
+ *
444
+ * @param err - The error object
445
+ * @param req - Express request object
446
+ * @param res - Express response object
447
+ * @param next - Express next function
448
+ */ const errorHandler = (err, req, res, next)=>{
449
+ console.error(err);
450
+ if (err instanceof RabApiError) {
451
+ var _err_errorCode;
452
+ return res.status(err.statusCode).send({
453
+ message: err.message,
454
+ errors: err.errors,
455
+ errorCode: (_err_errorCode = err.errorCode) != null ? _err_errorCode : err.statusCode
456
+ });
457
+ }
458
+ return res.status(500).send({
459
+ errors: [
460
+ {
461
+ message: 'Something went wrong'
462
+ }
463
+ ]
464
+ });
465
+ };
466
+
467
+ /**
468
+ * Formats Joi validation errors into a readable array of messages.
469
+ * @param error - The Joi validation error
470
+ * @returns Array of formatted error messages
471
+ */ function errorFormatter(error) {
472
+ const { details } = error;
473
+ return details.map((detail)=>detail.message.replace(/"/g, ''));
474
+ }
475
+ /**
476
+ * Validates data against a Joi schema.
477
+ * Throws a BadRequestException with detailed error messages if validation fails.
478
+ *
479
+ * @param schema - The Joi schema to validate against
480
+ * @param payload - The data to validate
481
+ * @param options - Optional Joi validation options
482
+ * @returns The validated and potentially transformed data
483
+ * @throws {BadRequestException} When validation fails
484
+ *
485
+ * @example
486
+ * ```typescript
487
+ * const schema = Joi.object({ email: Joi.string().email().required() });
488
+ * const validated = await joiValidator(schema, { email: 'test@example.com' });
489
+ * ```
490
+ */ async function joiValidator(schema, payload, options) {
491
+ try {
492
+ const value = await schema.validateAsync(payload, _extends({
493
+ abortEarly: false
494
+ }, options));
495
+ return value;
496
+ } catch (err) {
497
+ throw new BadRequestException('Invalid parameters', errorFormatter(err));
498
+ }
499
+ }
500
+ /**
501
+ * Validates data against a Joi schema (wrapper for joiValidator).
502
+ * @deprecated Use joiValidator directly
503
+ */ async function validateJoiSchema(scheme, data) {
504
+ return await joiValidator(scheme, data);
505
+ }
506
+ function retrieveRouteMetaData(route) {
507
+ const configuration = Reflect.getMetadata(CONTROLLER_ROUTE_KEY, route);
508
+ if (!configuration) {
509
+ throw new Error(`No @Post/@Get metadata found on controller: ${route.name}`);
510
+ }
511
+ return configuration;
512
+ }
513
+
514
+ var utils = /*#__PURE__*/Object.freeze({
515
+ __proto__: null,
516
+ joiValidator: joiValidator,
517
+ retrieveRouteMetaData: retrieveRouteMetaData,
518
+ validateJoiSchema: validateJoiSchema
519
+ });
520
+
521
+ const controllerHandler = (controller, config)=>{
522
+ return async (req, res, next)=>{
523
+ try {
524
+ let query = req.query;
525
+ if (config.validateQuery && req.query) {
526
+ if (config.parseQueryParams) {
527
+ query = await config.validateQuery(config.parseQueryParams(req.query));
528
+ } else {
529
+ query = await config.validateQuery(req.query);
530
+ }
531
+ }
532
+ const response = await controller.handler(_extends({}, req, {
533
+ query,
534
+ body: req.body
535
+ }));
536
+ var _response_statusCode, _response_statusCode1;
537
+ return res.status((_response_statusCode = response.statusCode) != null ? _response_statusCode : 200).json(response.excludeMetaData ? response.data : _extends({
538
+ message: 'successful',
539
+ statusCode: (_response_statusCode1 = response.statusCode) != null ? _response_statusCode1 : 200
540
+ }, response.data ? {
541
+ data: response.data
542
+ } : {
543
+ data: response
544
+ }));
545
+ } catch (error) {
546
+ if (error instanceof RabApiError) {
547
+ return next(new RabApiError(error.message, error.statusCode, error.errorCode, error.errors));
548
+ }
549
+ return next(error);
550
+ }
551
+ };
552
+ };
553
+
554
+ /**
555
+ * Extracts the Bearer token from the Authorization header
556
+ *
557
+ * @param request - Express request object
558
+ * @returns The extracted token or undefined if not found or not a Bearer token
559
+ *
560
+ * @example
561
+ * ```typescript
562
+ * const token = extractTokenFromHeader(req);
563
+ * if (!token) {
564
+ * throw new UnauthorizedException();
565
+ * }
566
+ * ```
567
+ */ const extractTokenFromHeader = (request)=>{
568
+ var _request_headers_authorization;
569
+ var _request_headers_authorization_split;
570
+ const [type, token] = (_request_headers_authorization_split = (_request_headers_authorization = request.headers.authorization) == null ? void 0 : _request_headers_authorization.split(' ')) != null ? _request_headers_authorization_split : [];
571
+ return type === 'Bearer' ? token : undefined;
572
+ };
573
+
574
+ const authHandler = (isProtected, config)=>(req, res, next)=>{
575
+ console.log('authHandler:', req.path, ':isProtected:', isProtected);
576
+ if (!isProtected) return next();
577
+ const token = extractTokenFromHeader(req);
578
+ if (!token) {
579
+ console.log('authHandler:UnauthorizedException:Token Not Found');
580
+ throw new UnauthorizedException();
581
+ }
582
+ try {
583
+ const payload = jwt.verify(token, config.jwt.secret_key);
584
+ req['auth'] = payload;
585
+ return next();
586
+ } catch (err) {
587
+ console.error('authHandler:JWT Error:', err.message);
588
+ throw new UnauthorizedException();
589
+ }
590
+ };
591
+
592
+ const bodyValidatorWithContext = (config)=>{
593
+ return async (req, res, next)=>{
594
+ // Skip if body validation is not needed
595
+ if (!config.bodySchema || config.disableBodyValidation || !req.body) {
596
+ return next();
597
+ }
598
+ try {
599
+ // Create a validation context with the authenticated user
600
+ const validationContext = {
601
+ 'atom-req-user': req.user || null
602
+ };
603
+ // Clone the schema and set context using validate options instead of prefs
604
+ // This avoids the "Cannot override context" error
605
+ const validationOptions = {
606
+ context: validationContext
607
+ };
608
+ // Validate the body with the user context using existing joiValidator
609
+ // Replace the request body with the validated body
610
+ req.body = await joiValidator(config.bodySchema, req.body, validationOptions);
611
+ next();
612
+ } catch (error) {
613
+ next(error);
614
+ }
615
+ };
616
+ };
617
+
618
+ function buildUrlPath(...segments) {
619
+ if (segments.length === 0) return '';
620
+ let pathSegments = [
621
+ ...segments
622
+ ].filter(Boolean);
623
+ // If last segment is an object (replacement map)
624
+ const last = pathSegments[pathSegments.length - 1];
625
+ if (typeof last === 'object' && last !== null) {
626
+ const replacements = last;
627
+ pathSegments = pathSegments.slice(0, -1).map((seg)=>{
628
+ if (typeof seg === 'string') {
629
+ let updated = seg;
630
+ for(const key in replacements){
631
+ updated = updated.replace(`:${key}`, String(replacements[key]));
632
+ }
633
+ return updated;
634
+ }
635
+ return seg;
636
+ });
637
+ }
638
+ return pathSegments.filter((segment)=>typeof segment === 'string' && segment.trim() !== '').map((segment, index)=>segment.startsWith('/') || index === 0 ? segment : '/' + segment).join('');
639
+ }
640
+
641
+ class OpenApiGenerator {
642
+ generate() {
643
+ var _this_options_openapi, _this_options_openapi1;
644
+ const info = ((_this_options_openapi = this.options.openapi) == null ? void 0 : _this_options_openapi.info) || {};
645
+ const spec = {
646
+ openapi: '3.0.3',
647
+ info: {
648
+ title: info.title || 'API Documentation',
649
+ version: info.version || '1.0.0',
650
+ description: info.description || 'Generated API documentation'
651
+ },
652
+ paths: {},
653
+ components: {
654
+ schemas: {},
655
+ securitySchemes: this.options.auth ? {
656
+ bearerAuth: {
657
+ type: 'http',
658
+ scheme: 'bearer',
659
+ bearerFormat: 'JWT',
660
+ description: 'JWT Bearer token authentication. Format: Authorization: Bearer <token>'
661
+ }
662
+ } : {}
663
+ }
664
+ };
665
+ // Add servers if specified
666
+ if ((_this_options_openapi1 = this.options.openapi) == null ? void 0 : _this_options_openapi1.servers) {
667
+ spec.servers = this.options.openapi.servers;
668
+ }
669
+ // Add security globally if auth is enabled
670
+ if (this.options.auth) {
671
+ spec.security = [
672
+ {
673
+ bearerAuth: []
674
+ }
675
+ ];
676
+ }
677
+ // Process collected routes
678
+ for (const route of this.routes){
679
+ var _route_options, _options_docs, _options_docs1, _options_docs2, _options_docs3, _options_docs4, _options_docs5;
680
+ // Skip if excluded from docs
681
+ if ((_route_options = route.options) == null ? void 0 : _route_options.excludeFromDocs) {
682
+ continue;
683
+ }
684
+ const { method, fullPath, options } = route;
685
+ let pathKey = fullPath.replace(/\/:([^/]+)/g, '/{$1}'); // Convert Express params to OpenAPI format
686
+ // Remove trailing slash if it exists (except for root path)
687
+ if (pathKey.length > 1 && pathKey.endsWith('/')) {
688
+ pathKey = pathKey.slice(0, -1);
689
+ }
690
+ if (!spec.paths[pathKey]) {
691
+ spec.paths[pathKey] = {};
692
+ }
693
+ const operation = {
694
+ summary: (options == null ? void 0 : (_options_docs = options.docs) == null ? void 0 : _options_docs.summary) || pathKey,
695
+ responses: (options == null ? void 0 : (_options_docs1 = options.docs) == null ? void 0 : _options_docs1.responses) || {
696
+ '200': {
697
+ description: 'Successful response',
698
+ content: {
699
+ 'application/json': {
700
+ schema: {
701
+ type: 'object'
702
+ }
703
+ }
704
+ }
705
+ },
706
+ '400': {
707
+ description: 'Bad request'
708
+ },
709
+ '401': {
710
+ description: 'Unauthorized'
711
+ },
712
+ '500': {
713
+ description: 'Internal server error'
714
+ }
715
+ }
716
+ };
717
+ // Add optional fields only if they exist
718
+ if (options == null ? void 0 : (_options_docs2 = options.docs) == null ? void 0 : _options_docs2.description) {
719
+ operation.description = options.docs.description;
720
+ }
721
+ // Add tags - use custom tags if provided, otherwise generate default tag from path
722
+ if (options == null ? void 0 : (_options_docs3 = options.docs) == null ? void 0 : _options_docs3.tags) {
723
+ operation.tags = options.docs.tags;
724
+ } else {
725
+ // Generate default tag from the first segment of the path
726
+ const defaultTag = this.generateDefaultTag(fullPath);
727
+ if (defaultTag) {
728
+ operation.tags = [
729
+ defaultTag
730
+ ];
731
+ }
732
+ }
733
+ if (options == null ? void 0 : (_options_docs4 = options.docs) == null ? void 0 : _options_docs4.operationId) {
734
+ operation.operationId = options.docs.operationId;
735
+ }
736
+ if (options == null ? void 0 : (_options_docs5 = options.docs) == null ? void 0 : _options_docs5.deprecated) {
737
+ operation.deprecated = options.docs.deprecated;
738
+ }
739
+ // Add security for protected routes
740
+ // First check if docs specify custom security, otherwise use default logic
741
+ if ((options == null ? void 0 : options.isProtected) !== false && this.options.auth) {
742
+ operation.security = [
743
+ {
744
+ bearerAuth: []
745
+ }
746
+ ];
747
+ } else if ((options == null ? void 0 : options.isProtected) === false) {
748
+ operation.security = [];
749
+ }
750
+ // Add request body for POST/PUT/PATCH
751
+ if ([
752
+ 'post',
753
+ 'put',
754
+ 'patch'
755
+ ].includes(method) && (options == null ? void 0 : options.bodySchema)) {
756
+ operation.requestBody = {
757
+ required: true,
758
+ content: {
759
+ 'application/json': {
760
+ schema: this.joiToOpenApiSchema(options.bodySchema)
761
+ }
762
+ }
763
+ };
764
+ }
765
+ // Add query parameters if querySchema exists
766
+ if (options == null ? void 0 : options.querySchema) {
767
+ const querySchema = this.joiToOpenApiSchema(options.querySchema);
768
+ if (querySchema.properties) {
769
+ const queryParams = Object.entries(querySchema.properties).map(([name, schema])=>{
770
+ var _querySchema_required;
771
+ return {
772
+ name,
773
+ in: 'query',
774
+ required: ((_querySchema_required = querySchema.required) == null ? void 0 : _querySchema_required.includes(name)) || false,
775
+ schema
776
+ };
777
+ });
778
+ // Only add parameters if we have query parameters
779
+ if (queryParams.length > 0) {
780
+ operation.parameters = queryParams;
781
+ }
782
+ }
783
+ }
784
+ spec.paths[pathKey][method] = operation;
785
+ }
786
+ return spec;
787
+ }
788
+ joiToOpenApiSchema(joiSchema) {
789
+ if (!joiSchema || typeof joiSchema.describe !== 'function') {
790
+ return {
791
+ type: 'object'
792
+ };
793
+ }
794
+ try {
795
+ // Use Joi's describe() method to get the schema structure
796
+ const description = joiSchema.describe();
797
+ return this.convertJoiDescriptionToOpenApi(description);
798
+ } catch (error) {
799
+ console.warn('Failed to convert Joi schema to OpenAPI:', error);
800
+ return {
801
+ type: 'object'
802
+ };
803
+ }
804
+ }
805
+ convertJoiDescriptionToOpenApi(joiDescription) {
806
+ var _joiDescription_flags, _joiDescription_flags1;
807
+ const schema = {};
808
+ // Handle different Joi types
809
+ switch(joiDescription.type){
810
+ case 'object':
811
+ schema.type = 'object';
812
+ if (joiDescription.keys) {
813
+ schema.properties = {};
814
+ const required = [];
815
+ for (const [key, value] of Object.entries(joiDescription.keys)){
816
+ var _keyDescription_flags;
817
+ const keyDescription = value;
818
+ schema.properties[key] = this.convertJoiDescriptionToOpenApi(keyDescription);
819
+ // Check if field is required
820
+ if (((_keyDescription_flags = keyDescription.flags) == null ? void 0 : _keyDescription_flags.presence) === 'required') {
821
+ required.push(key);
822
+ }
823
+ }
824
+ if (required.length > 0) {
825
+ schema.required = required;
826
+ }
827
+ }
828
+ break;
829
+ case 'array':
830
+ schema.type = 'array';
831
+ if (joiDescription.items && joiDescription.items.length > 0) {
832
+ schema.items = this.convertJoiDescriptionToOpenApi(joiDescription.items[0]);
833
+ }
834
+ break;
835
+ case 'string':
836
+ schema.type = 'string';
837
+ if (joiDescription.rules) {
838
+ for (const rule of joiDescription.rules){
839
+ switch(rule.name){
840
+ case 'min':
841
+ var _rule_args;
842
+ schema.minLength = (_rule_args = rule.args) == null ? void 0 : _rule_args.limit;
843
+ break;
844
+ case 'max':
845
+ var _rule_args1;
846
+ schema.maxLength = (_rule_args1 = rule.args) == null ? void 0 : _rule_args1.limit;
847
+ break;
848
+ case 'email':
849
+ schema.format = 'email';
850
+ break;
851
+ case 'uri':
852
+ schema.format = 'uri';
853
+ break;
854
+ case 'uuid':
855
+ schema.format = 'uuid';
856
+ break;
857
+ }
858
+ }
859
+ }
860
+ if (joiDescription.allow) {
861
+ schema.enum = joiDescription.allow;
862
+ }
863
+ break;
864
+ case 'number':
865
+ schema.type = 'number';
866
+ if (joiDescription.rules) {
867
+ for (const rule of joiDescription.rules){
868
+ switch(rule.name){
869
+ case 'min':
870
+ var _rule_args2;
871
+ schema.minimum = (_rule_args2 = rule.args) == null ? void 0 : _rule_args2.limit;
872
+ break;
873
+ case 'max':
874
+ var _rule_args3;
875
+ schema.maximum = (_rule_args3 = rule.args) == null ? void 0 : _rule_args3.limit;
876
+ break;
877
+ case 'integer':
878
+ schema.type = 'integer';
879
+ break;
880
+ }
881
+ }
882
+ }
883
+ break;
884
+ case 'boolean':
885
+ schema.type = 'boolean';
886
+ break;
887
+ case 'date':
888
+ schema.type = 'string';
889
+ schema.format = 'date-time';
890
+ break;
891
+ default:
892
+ schema.type = 'object';
893
+ }
894
+ // Add description if available
895
+ if ((_joiDescription_flags = joiDescription.flags) == null ? void 0 : _joiDescription_flags.description) {
896
+ schema.description = joiDescription.flags.description;
897
+ }
898
+ // Add default value if available
899
+ if (((_joiDescription_flags1 = joiDescription.flags) == null ? void 0 : _joiDescription_flags1.default) !== undefined) {
900
+ schema.default = joiDescription.flags.default;
901
+ }
902
+ // Handle nullable values
903
+ if (joiDescription.allow && joiDescription.allow.includes(null)) {
904
+ schema.nullable = true;
905
+ }
906
+ return schema;
907
+ }
908
+ generateDefaultTag(fullPath) {
909
+ // Extract the first meaningful segment from the path to use as tag
910
+ // e.g., "/users/profile" -> "Users", "/api/v1/products" -> "Products"
911
+ const segments = fullPath.split('/').filter((segment)=>segment && segment !== 'api' && !segment.match(/^v\d+$/) // Skip version segments like v1, v2
912
+ );
913
+ if (segments.length === 0) {
914
+ return null;
915
+ }
916
+ const firstSegment = segments[0];
917
+ // Capitalize first letter and make it singular/clean
918
+ return firstSegment.charAt(0).toUpperCase() + firstSegment.slice(1).toLowerCase();
919
+ }
920
+ constructor(routes, options){
921
+ this.routes = routes;
922
+ this.options = options;
923
+ }
924
+ }
925
+
926
+ function isRouteAController(value) {
927
+ return typeof value === 'function'; // controllers are class and this will return constructor function
928
+ }
929
+ class RabApi {
930
+ static createRouter(props) {
931
+ return props;
932
+ }
933
+ static createApp(props) {
934
+ return new AtomExpressApp(props);
935
+ }
936
+ }
937
+ class AtomExpressApp {
938
+ use(...middleware) {
939
+ this.app.use(...middleware);
940
+ }
941
+ resolvePipes(routeDefinition = {}, pipes) {
942
+ const resolvedPipes = [];
943
+ // Process pipes
944
+ if (pipes) {
945
+ for (const pipe of pipes){
946
+ if (isCallBackPipe(pipe)) {
947
+ const callbackPipes = pipe(routeDefinition);
948
+ resolvedPipes.push(...callbackPipes);
949
+ } else {
950
+ resolvedPipes.push(pipe);
951
+ }
952
+ }
953
+ }
954
+ return resolvedPipes;
955
+ }
956
+ route(routerParams) {
957
+ this.app.use(routerParams.basePath || '', this.buildRouter(_extends({}, routerParams, {
958
+ fullPath: routerParams.basePath || ''
959
+ })));
960
+ }
961
+ listen(port, callback) {
962
+ var _this_options_openapi;
963
+ // Setup OpenAPI endpoint if enabled
964
+ if (((_this_options_openapi = this.options.openapi) == null ? void 0 : _this_options_openapi.enabled) !== false) {
965
+ var _this_options_openapi1;
966
+ const openapiPath = ((_this_options_openapi1 = this.options.openapi) == null ? void 0 : _this_options_openapi1.path) || '/openapi.json';
967
+ const generator = new OpenApiGenerator(this.collectedRoutes, this.options);
968
+ this.app.get(openapiPath, (req, res)=>{
969
+ res.json(generator.generate());
970
+ });
971
+ }
972
+ if (this.options.errorHandler) {
973
+ this.app.use(this.options.errorHandler);
974
+ }
975
+ return this.app.listen(port, callback);
976
+ }
977
+ getCollectedRoutes() {
978
+ return this.collectedRoutes;
979
+ }
980
+ constructor(options){
981
+ var _options_auth;
982
+ this.collectedRoutes = [];
983
+ this.setupRouteController = (expressRouter, route, parentOptions)=>{
984
+ var _config_docs;
985
+ const metaData = retrieveRouteMetaData(route);
986
+ const { method, path, options } = metaData;
987
+ const config = _extends({}, parentOptions, options, {
988
+ enforceBodyValidation: this.options.enforceBodyValidation
989
+ });
990
+ if (method == 'get' && config.querySchema && !config.validateQuery) {
991
+ const querySchema = config.querySchema;
992
+ config.validateQuery = (body)=>validateJoiSchema(querySchema, body);
993
+ }
994
+ if ([
995
+ 'post',
996
+ 'put',
997
+ 'patch'
998
+ ].includes(method)) {
999
+ if (!config.disableBodyValidation) {
1000
+ if (config.enforceBodyValidation && !config.bodySchema) {
1001
+ throw new Error('missing body schema: your api enforce body validation');
1002
+ }
1003
+ }
1004
+ }
1005
+ var _parentOptions_pipes, _options_pipes;
1006
+ const allPipes = this.resolvePipes(config, [
1007
+ ...(_parentOptions_pipes = parentOptions == null ? void 0 : parentOptions.pipes) != null ? _parentOptions_pipes : [],
1008
+ ...(_options_pipes = options == null ? void 0 : options.pipes) != null ? _options_pipes : []
1009
+ ]);
1010
+ //auth middleware
1011
+ if (this.options.auth) {
1012
+ var _config_isProtected;
1013
+ allPipes.unshift(authHandler((_config_isProtected = config.isProtected) != null ? _config_isProtected : this.options.enforceRouteProtection, this.options.auth));
1014
+ }
1015
+ //add body validation to validate the schema and inject request context
1016
+ if (config.bodySchema && !config.disableBodyValidation) {
1017
+ allPipes.push(bodyValidatorWithContext(config));
1018
+ }
1019
+ const allMiddlewares = [
1020
+ ...allPipes,
1021
+ controllerHandler(typedi.Container.get(route), config)
1022
+ ];
1023
+ expressRouter[method](path || '', composeMiddleware.compose(allMiddlewares));
1024
+ // Collect route information for OpenAPI
1025
+ const fullPath = buildUrlPath((parentOptions == null ? void 0 : parentOptions.fullPath) || '', path);
1026
+ this.collectedRoutes.push({
1027
+ method,
1028
+ path,
1029
+ fullPath,
1030
+ controller: route,
1031
+ metadata: metaData,
1032
+ options: _extends({}, config, {
1033
+ docs: _extends({}, config.docs, {
1034
+ tags: ((_config_docs = config.docs) == null ? void 0 : _config_docs.tags) || (parentOptions == null ? void 0 : parentOptions.tags)
1035
+ })
1036
+ })
1037
+ });
1038
+ };
1039
+ this.buildRouter = (routerParams)=>{
1040
+ const expressRouter = express.Router();
1041
+ const { controllers } = routerParams, options = _object_without_properties_loose(routerParams, [
1042
+ "controllers"
1043
+ ]);
1044
+ for (const route of controllers){
1045
+ if (isRouteAController(route)) {
1046
+ this.setupRouteController(expressRouter, route, options);
1047
+ } else {
1048
+ var _routerParams_pipes, _route_pipes;
1049
+ expressRouter.use(route.basePath || '', this.buildRouter(_extends({}, route, {
1050
+ pipes: [
1051
+ ...(_routerParams_pipes = routerParams.pipes) != null ? _routerParams_pipes : [],
1052
+ ...(_route_pipes = route == null ? void 0 : route.pipes) != null ? _route_pipes : []
1053
+ ],
1054
+ fullPath: buildUrlPath(routerParams.basePath, route.basePath)
1055
+ })));
1056
+ }
1057
+ }
1058
+ return expressRouter;
1059
+ };
1060
+ this.app = express();
1061
+ if (((_options_auth = options.auth) == null ? void 0 : _options_auth.jwt) && options.auth.jwt.secret_key && options.auth.jwt.secret_key.length <= 10) {
1062
+ throw new Error('AtomApi:JWT:Error: missing secret or secret key length must be greater than 10');
1063
+ }
1064
+ var _options_enforceRouteProtection, _options_enforceBodyValidation;
1065
+ this.options = _extends({}, options, {
1066
+ enforceRouteProtection: (_options_enforceRouteProtection = options.enforceRouteProtection) != null ? _options_enforceRouteProtection : true,
1067
+ enforceBodyValidation: (_options_enforceBodyValidation = options.enforceBodyValidation) != null ? _options_enforceBodyValidation : true
1068
+ });
1069
+ }
1070
+ }
1071
+
1072
+ exports.AtomExpressApp = AtomExpressApp;
1073
+ exports.AtomHelpers = utils;
1074
+ exports.AtomRoute = AtomRoute;
1075
+ exports.BadRequestException = BadRequestException;
1076
+ exports.CONTROLLER_ROUTE_KEY = CONTROLLER_ROUTE_KEY;
1077
+ exports.ConflictException = ConflictException;
1078
+ exports.Controller = Controller;
1079
+ exports.Delete = Delete;
1080
+ exports.DiContainer = DiContainer;
1081
+ exports.ForbiddenException = ForbiddenException;
1082
+ exports.Get = Get;
1083
+ exports.Injectable = Injectable;
1084
+ exports.InternalServerErrorException = InternalServerErrorException;
1085
+ exports.MethodNotAllowedException = MethodNotAllowedException;
1086
+ exports.NotFoundException = NotFoundException;
1087
+ exports.OpenApiGenerator = OpenApiGenerator;
1088
+ exports.Patch = Patch;
1089
+ exports.PayloadTooLargeException = PayloadTooLargeException;
1090
+ exports.Post = Post;
1091
+ exports.Put = Put;
1092
+ exports.RabApi = RabApi;
1093
+ exports.RabApiError = RabApiError;
1094
+ exports.RequestTimeoutException = RequestTimeoutException;
1095
+ exports.ServiceUnavailableException = ServiceUnavailableException;
1096
+ exports.TooManyRequestsException = TooManyRequestsException;
1097
+ exports.UnauthorizedException = UnauthorizedException;
1098
+ exports.UnprocessableEntityException = UnprocessableEntityException;
1099
+ exports.authHandler = authHandler;
1100
+ exports.bodyValidatorWithContext = bodyValidatorWithContext;
1101
+ exports.controllerHandler = controllerHandler;
1102
+ exports.errorHandler = errorHandler;
1103
+ exports.extractTokenFromHeader = extractTokenFromHeader;
1104
+ exports.isCallBackPipe = isCallBackPipe;
1105
+ exports.isRouteAController = isRouteAController;