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