@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/README.md +393 -0
- package/index.cjs.d.ts +1 -0
- package/index.cjs.js +1105 -0
- package/index.esm.d.ts +966 -0
- package/index.esm.js +1070 -0
- package/package.json +30 -0
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 };
|