te.js 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/example/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import Tejas from 'te.js';
2
2
 
3
3
  const tejas = new Tejas();
4
- tejas.takeoff();
4
+ tejas.takeoff();
@@ -1,11 +1,7 @@
1
- import { Target } from 'te.js';
1
+ import { Target, TejError } from 'te.js';
2
2
 
3
3
  const target = new Target();
4
4
 
5
- target.register('/hello', (ammo) => {
6
- throw new Error('Error thrown to demonstrate robust error handling of te.js');
7
- ammo.fire({
8
- status: 200,
9
- body: 'Hello, World!'
10
- });
5
+ target.register('/', (ammo) => {
6
+ throw new TejError(500, 'Hello')
11
7
  });
@@ -6,9 +6,5 @@
6
6
  },
7
7
  "dir":{
8
8
  "targets": "targets"
9
- },
10
- "db": {
11
- "type": "mongodb",
12
- "uri": "mongodb+srv://sheldon-cooper:J1T6P1rK4TYXCzUB@adb-cloaker.bhw9t.mongodb.net/retag-cloaker?retryWrites=true&w=majority"
13
9
  }
14
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "te.js",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "A nodejs framework",
5
5
  "type": "module",
6
6
  "main": "te.js",
@@ -1,48 +1,98 @@
1
1
  import status from 'statuses';
2
2
 
3
3
  const formattedData = (data) => {
4
- if (typeof data === 'object') return JSON.stringify(data);
4
+ if (data === null || data === undefined) return '';
5
+
6
+ if (typeof data === 'object') {
7
+ try {
8
+ return JSON.stringify(data);
9
+ } catch (error) {
10
+ return String(data);
11
+ }
12
+ }
13
+
5
14
  if (typeof data === 'string') return data;
6
- if (typeof data === 'number') return status[data];
7
- return data;
15
+ if (typeof data === 'number') return status[data] || String(data);
16
+
17
+ return String(data);
8
18
  };
9
19
 
10
20
  const statusAndData = (args) => {
11
- if (!args || args.length === 0)
21
+ // Handle no arguments
22
+ if (!args || args.length === 0) {
12
23
  return {
13
24
  statusCode: 204,
14
25
  data: status(204),
15
26
  contentType: 'text/plain',
16
27
  };
28
+ }
29
+
30
+ // Handle single argument
31
+ if (args.length === 1) {
32
+ const arg = args[0];
17
33
 
18
- if (args.length === 1 && typeof args[0] === 'number')
34
+ // If it's a number, treat as status code
35
+ if (typeof arg === 'number') {
36
+ return {
37
+ statusCode: arg,
38
+ data: status(arg) || String(arg),
39
+ contentType: 'text/plain',
40
+ };
41
+ }
42
+
43
+ // Otherwise treat as data
19
44
  return {
20
- statusCode: args[0],
21
- data: status(args[0]),
22
- contentType: 'text/plain',
45
+ statusCode: 200,
46
+ data: formattedData(arg),
47
+ contentType: contentType(arg),
23
48
  };
49
+ }
24
50
 
51
+ // Handle multiple arguments
25
52
  let statusCode = 200;
26
53
  let data = args[0];
27
- if (args.length > 1) {
54
+
55
+ // If first argument is a number, treat as status code
56
+ if (typeof args[0] === 'number') {
28
57
  statusCode = args[0];
29
58
  data = args[1];
30
- if (!data) data = status[statusCode];
59
+ } else {
60
+ // If first argument is not a number, check if second is
61
+ if (typeof args[1] === 'number') {
62
+ statusCode = args[1];
63
+ }
64
+ }
65
+
66
+ // If data is undefined, use status message
67
+ if (data === undefined) {
68
+ data = status[statusCode] || String(statusCode);
31
69
  }
32
70
 
71
+ // If third argument is provided, it's the content type
72
+ const customContentType = args.length > 2 ? args[2] : null;
73
+
33
74
  return {
34
75
  statusCode,
35
76
  data: formattedData(data),
36
- contentType: contentType(data),
77
+ contentType: customContentType || contentType(data),
37
78
  };
38
79
  };
39
80
 
40
81
  const contentType = (data) => {
82
+ if (data === null || data === undefined) return 'text/plain';
83
+
41
84
  switch (typeof data) {
42
85
  case 'object':
43
86
  return 'application/json';
44
87
  case 'string':
45
- return 'text/html';
88
+ // Check if string is HTML
89
+ if (
90
+ data.trim().toLowerCase().startsWith('<!DOCTYPE') ||
91
+ data.trim().toLowerCase().startsWith('<html')
92
+ ) {
93
+ return 'text/html';
94
+ }
95
+ return 'text/plain';
46
96
  case 'number':
47
97
  return 'text/plain';
48
98
  default:
@@ -50,4 +100,4 @@ const contentType = (data) => {
50
100
  }
51
101
  };
52
102
 
53
- export { statusAndData, contentType };
103
+ export { statusAndData, contentType, formattedData };
package/server/ammo.js CHANGED
@@ -8,7 +8,32 @@ import html from '../utils/tejas-entrypoint-html.js';
8
8
  import ammoEnhancer from './ammo/enhancer.js';
9
9
  import TejError from './error.js';
10
10
 
11
+ /**
12
+ * Ammo class for handling HTTP requests and responses.
13
+ *
14
+ * @description
15
+ * Ammo is a utility class that simplifies HTTP request handling and response generation.
16
+ * It provides methods for processing requests, sending responses, and handling errors.
17
+ *
18
+ * @example
19
+ *
20
+ * if (ammo.GET) {
21
+ * ammo.fire(200, { message: 'Hello World' });
22
+ * } else {
23
+ * ammo.notAllowed();
24
+ * }
25
+ */
11
26
  class Ammo {
27
+ /**
28
+ * Creates a new Ammo instance.
29
+ *
30
+ * @param {http.IncomingMessage} req - The HTTP request object
31
+ * @param {http.ServerResponse} res - The HTTP response object
32
+ *
33
+ * @description
34
+ * Initializes a new Ammo instance with the provided request and response objects.
35
+ * Sets up default values for various properties that will be populated by the enhance method.
36
+ */
12
37
  constructor(req, res) {
13
38
  this.req = req;
14
39
  this.res = res;
@@ -39,6 +64,24 @@ class Ammo {
39
64
  this.dispatchedData = undefined;
40
65
  }
41
66
 
67
+ /**
68
+ * Enhances the Ammo instance with request data and sets HTTP method flags.
69
+ *
70
+ * @description
71
+ * This method processes the request and sets various properties on the Ammo instance:
72
+ * - HTTP method flags (GET, POST, PUT, etc.)
73
+ * - Request data (IP, headers, payload, method)
74
+ * - URL data (protocol, hostname, path, endpoint, fullURL)
75
+ *
76
+ * This method should be called before using any other Ammo methods.
77
+ *
78
+ * @returns {Promise<void>} A promise that resolves when enhancement is complete
79
+ *
80
+ * @example
81
+ * const ammo = new Ammo(req, res);
82
+ * await ammo.enhance();
83
+ * // Now you can use ammo.GET, ammo.path, etc.
84
+ */
42
85
  async enhance() {
43
86
  await ammoEnhancer(this);
44
87
 
@@ -51,6 +94,52 @@ class Ammo {
51
94
  this.OPTIONS = this.method === 'OPTIONS';
52
95
  }
53
96
 
97
+ /**
98
+ * Sends a response to the client with the specified data and status code.
99
+ *
100
+ * @param {number|any} [arg1] - If a number, treated as status code. Otherwise treated as data to send.
101
+ * @param {any} [arg2] - If arg1 is a number, this is the data to send. Otherwise ignored.
102
+ * @param {string} [arg3] - Optional content type override.
103
+ *
104
+ * @description
105
+ * The fire method is flexible and can handle different argument patterns:
106
+ *
107
+ * 1. No arguments: Sends a 204 No Content response
108
+ * 2. Single number: Sends a response with the given status code
109
+ * 3. Single non-number: Sends a 200 OK response with the given data
110
+ * 4. Two arguments (number, data): Sends a response with the given status code and data
111
+ * 5. Three arguments: Sends a response with the given status code, data, and content type
112
+ *
113
+ * The fire method can be used with any HTTP status code, including error codes (4xx, 5xx).
114
+ * For error responses, you can use either fire() or throw(). The main difference is that
115
+ * throw() can accept an Error instance and has special handling for it, while fire() only
116
+ * accepts status codes, strings, or other data types.
117
+ *
118
+ * @example
119
+ * // Send a 200 OK response with JSON data
120
+ * ammo.fire(200, { message: 'Success' });
121
+ *
122
+ * @example
123
+ * // Send a 404 Not Found response with custom message
124
+ * ammo.fire(404, 'Resource not found');
125
+ *
126
+ * @example
127
+ * // Send a 500 Internal Server Error response
128
+ * ammo.fire(500, 'Something went wrong');
129
+ *
130
+ * @example
131
+ * // Send HTML content with custom content type
132
+ * ammo.fire(200, '<html><body>Hello</body></html>', 'text/html');
133
+ *
134
+ * @example
135
+ * // Send just a status code (will use default status message)
136
+ * ammo.fire(204);
137
+ *
138
+ * @example
139
+ * // Send just data (will use 200 status code)
140
+ * ammo.fire({ message: 'Success' });
141
+ * ammo.fire('Hello World');
142
+ */
54
143
  fire() {
55
144
  const { statusCode, data, contentType } = statusAndData(arguments);
56
145
  const contentTypeHeader = { 'Content-Type': contentType };
@@ -62,50 +151,176 @@ class Ammo {
62
151
  this.res.end();
63
152
  }
64
153
 
154
+ /**
155
+ * Throws a 404 Not Found error.
156
+ *
157
+ * @description
158
+ * This is a convenience method that throws a 404 Not Found error.
159
+ * It's equivalent to calling `throw(404) ` or `fire(404)`.
160
+ *
161
+ * @throws {TejError} Always throws a TejError with status code 404
162
+ *
163
+ * @example
164
+ * // If resource not found
165
+ * if (!resource) {
166
+ * ammo.notFound();
167
+ * }
168
+ */
65
169
  notFound() {
66
170
  throw new TejError(404, 'Not Found');
67
171
  }
68
172
 
173
+ /**
174
+ * Throws a 405 Method Not Allowed error.
175
+ *
176
+ * @description
177
+ * This is a convenience method that throws a 405 Method Not Allowed error.
178
+ * It's equivalent to calling `throw(405)` or `fire(405)`.
179
+ *
180
+ * @throws {TejError} Always throws a TejError with status code 405
181
+ *
182
+ * @example
183
+ * // If method not allowed
184
+ * if (!allowedMethods.includes(ammo.method)) {
185
+ * ammo.notAllowed();
186
+ * }
187
+ */
69
188
  notAllowed() {
70
189
  throw new TejError(405, 'Method Not Allowed');
71
190
  }
72
191
 
192
+ /**
193
+ * Throws a 401 Unauthorized error.
194
+ *
195
+ * @description
196
+ * This is a convenience method that throws a 401 Unauthorized error.
197
+ * It's equivalent to calling `throw(401) ` or `fire(401)`.
198
+ *
199
+ * @throws {TejError} Always throws a TejError with status code 401
200
+ *
201
+ * @example
202
+ * // If user is not authenticated
203
+ * if (!user) {
204
+ * ammo.unauthorized();
205
+ * }
206
+ */
73
207
  unauthorized() {
74
208
  throw new TejError(401, 'Unauthorized');
75
209
  }
76
210
 
211
+ /**
212
+ * Sends the default entry point HTML.
213
+ *
214
+ * @description
215
+ * This method sends the default HTML entry point for the application.
216
+ * It's typically used as a fallback when no specific route is matched.
217
+ *
218
+ * @example
219
+ * // In a catch-all route
220
+ * ammo.defaultEntry();
221
+ */
77
222
  defaultEntry() {
78
223
  this.fire(html);
79
224
  }
80
225
 
226
+ /**
227
+ * Throws an error response with appropriate status code and message.
228
+ *
229
+ * @param {number|Error|string} [arg1] - Status code, Error object, or error message
230
+ * @param {string} [arg2] - Error message (only used when arg1 is a status code)
231
+ *
232
+ * @description
233
+ * The throw method is flexible and can handle different argument patterns:
234
+ *
235
+ * 1. No arguments: Sends a 500 Internal Server Error response
236
+ * 2. Status code: Sends a response with the given status code and default message
237
+ * 3. Status code and message: Sends a response with the given status code and message
238
+ * 4. Error object: Extracts status code and message from the error
239
+ * 5. String: Treats as error message with 500 status code
240
+ *
241
+ * The key difference between throw() and fire() is that throw() can accept an Error instance
242
+ * and has special handling for it. Internally, throw() still calls fire() to send the response.
243
+ *
244
+ * @example
245
+ * // Throw a 404 Not Found error
246
+ * ammo.throw(404);
247
+ *
248
+ * @example
249
+ * // Throw a 404 Not Found error with custom message
250
+ * ammo.throw(404, 'Resource not found');
251
+ *
252
+ * @example
253
+ * // Throw an error from an Error object
254
+ * ammo.throw(new Error('Something went wrong'));
255
+ *
256
+ * @example
257
+ * // Throw an error with a custom message
258
+ * ammo.throw('Something went wrong');
259
+ */
81
260
  throw() {
82
- const errCode = arguments[0];
83
- const err = arguments[1];
261
+ // Handle different argument patterns
262
+ const args = Array.from(arguments);
84
263
 
85
- let errMsg = err instanceof Error ? err.message : err.toString();
264
+ // Case 1: No arguments provided
265
+ if (args.length === 0) {
266
+ this.fire(500, 'Internal Server Error');
267
+ return;
268
+ }
86
269
 
87
- if (errCode && isStatusCode(errCode)) {
88
- if (!errMsg) errMsg = toStatusMessage(errCode);
89
- this.fire(errCode, errMsg);
270
+ // Case 2: First argument is a status code
271
+ if (isStatusCode(args[0])) {
272
+ const statusCode = args[0];
273
+ const message = args[1] || toStatusMessage(statusCode);
274
+ this.fire(statusCode, message);
90
275
  return;
91
276
  }
92
277
 
93
- if (err instanceof Error) {
94
- const errMessage = err.message;
278
+ // Case 3.1: First argument is an instance of TejError
279
+ if (args[0] instanceof TejError) {
280
+ const error = args[0];
281
+ const statusCode = error.code;
282
+ const message = error.message;
95
283
 
96
- if (!isNaN(parseInt(errMessage))) {
97
- // Execute when errMessage is a number. Notice ! in front of isNan
98
- const message = toStatusMessage(errMessage) ?? toStatusMessage(500);
99
- this.fire(message, message);
284
+ this.fire(statusCode, message);
285
+ return;
286
+ }
287
+
288
+ // Case 3.2: First argument is an Error object
289
+ if (args[0] instanceof Error) {
290
+ const error = args[0];
291
+
292
+ // Check if error message is a numeric status code
293
+ if (!isNaN(parseInt(error.message))) {
294
+ const statusCode = parseInt(error.message);
295
+ const message = toStatusMessage(statusCode) || toStatusMessage(500);
296
+ this.fire(statusCode, message);
100
297
  return;
101
298
  }
102
299
 
103
- const code = toStatusCode(errMsg) ?? 500;
104
- this.fire(code, errMsg);
300
+ // Use error message as status code if it's a valid status code string
301
+ const statusCode = toStatusCode(error.message);
302
+ if (statusCode) {
303
+ this.fire(statusCode, error.message);
304
+ return;
305
+ }
306
+
307
+ // Default error handling
308
+ this.fire(500, error.message);
309
+ return;
310
+ }
311
+
312
+ // Case 4: First argument is a string or other value
313
+ const errorValue = args[0];
314
+
315
+ // Check if the string represents a status code
316
+ const statusCode = toStatusCode(errorValue);
317
+ if (statusCode) {
318
+ this.fire(statusCode, toStatusMessage(statusCode));
105
319
  return;
106
320
  }
107
321
 
108
- this.fire(err);
322
+ // Default case: treat as error message
323
+ this.fire(500, errorValue.toString());
109
324
  }
110
325
  }
111
326
 
@@ -0,0 +1,53 @@
1
+ import isMiddlewareValid from './targets/middleware-validator.js';
2
+ import { isPathValid, standardizePath } from './targets/path-validator.js';
3
+ import isShootValid from './targets/shoot-validator.js';
4
+
5
+ class Endpoint {
6
+ constructor() {
7
+ this.path = '';
8
+ this.middlewares = [];
9
+ this.handler = null;
10
+ }
11
+
12
+ setPath(base, path) {
13
+ const standardizedBase = standardizePath(base);
14
+ const standardizedPath = standardizePath(path);
15
+
16
+ let fullPath = `${standardizedBase}${standardizedPath}`;
17
+ if (fullPath.length === 0) fullPath = '/';
18
+
19
+ if (!isPathValid(fullPath)) return this;
20
+
21
+ this.path = fullPath;
22
+ return this;
23
+ }
24
+
25
+ setMiddlewares(middlewares) {
26
+ const validMiddlewares = middlewares.filter(isMiddlewareValid);
27
+ if (validMiddlewares.length === 0) return this;
28
+
29
+ this.middlewares = this.middlewares.concat(validMiddlewares);
30
+ return this;
31
+ }
32
+
33
+ setHandler(handler) {
34
+ if (!isShootValid(handler)) return this;
35
+ this.handler = handler;
36
+
37
+ return this;
38
+ }
39
+
40
+ getPath() {
41
+ return this.path;
42
+ }
43
+
44
+ getMiddlewares() {
45
+ return this.middlewares;
46
+ }
47
+
48
+ getHandler() {
49
+ return this.handler;
50
+ }
51
+ }
52
+
53
+ export default Endpoint;
package/server/handler.js CHANGED
@@ -1,72 +1,87 @@
1
- import { env } from 'tej-env';
2
- import TejLogger from 'tej-logger';
3
- import logHttpRequest from '../utils/request-logger.js';
4
-
5
- import Ammo from './ammo.js';
6
- import TejError from './error.js';
7
- import TargetRegistry from './targets/registry.js';
8
-
9
- const targetRegistry = new TargetRegistry();
10
- const errorLogger = new TejLogger('Tejas.Exception');
11
-
12
- const executeChain = async (target, ammo) => {
13
- let i = 0;
14
-
15
- const chain = targetRegistry.globalMiddlewares.concat(target.middlewares);
16
- chain.push(target.shoot);
17
-
18
- const next = async () => {
19
- const middleware = chain[i];
20
- i++;
21
-
22
- const args =
23
- middleware.length === 3 ? [ammo.req, ammo.res, next] : [ammo, next];
24
-
25
- try {
26
- await middleware(...args);
27
- } catch (err) {
28
- errorHandler(ammo, err);
29
- }
30
- };
31
-
32
- await next();
33
- };
34
-
35
- const errorHandler = (ammo, err) => {
36
- if (env('LOG_EXCEPTIONS')) errorLogger.error(err);
37
-
38
- if (err instanceof TejError) return ammo.throw(err.code, err);
39
-
40
- ammo.throw(500, err);
41
- };
42
-
43
- const handler = async (req, res) => {
44
- const target = targetRegistry.aim(req.method, req.url.split('?')[0]);
45
- const ammo = new Ammo(req, res);
46
-
47
- try {
48
- if (target) {
49
- await ammo.enhance();
50
-
51
- if (env('LOG_HTTP_REQUESTS')) logHttpRequest(ammo);
52
- await executeChain(target, ammo);
53
-
54
- } else {
55
- if (req.url === '/') {
56
- ammo.defaultEntry();
57
- } else {
58
- errorHandler(
59
- ammo,
60
- new TejError(
61
- 404,
62
- `No target found for URL ${ammo.fullURL} with method ${ammo.method}`,
63
- ),
64
- );
65
- }
66
- }
67
- } catch (err) {
68
- errorHandler(ammo, err);
69
- }
70
- };
71
-
72
- export default handler;
1
+ import { env } from 'tej-env';
2
+ import TejLogger from 'tej-logger';
3
+ import logHttpRequest from '../utils/request-logger.js';
4
+
5
+ import Ammo from './ammo.js';
6
+ import TejError from './error.js';
7
+ import TargetRegistry from './targets/registry.js';
8
+
9
+ const targetRegistry = new TargetRegistry();
10
+ const errorLogger = new TejLogger('Tejas.Exception');
11
+
12
+ /**
13
+ * Executes the middleware and handler chain for a given target.
14
+ *
15
+ * @param {Object} target - The target endpoint object.
16
+ * @param {Ammo} ammo - The Ammo instance containing request and response objects.
17
+ * @returns {Promise<void>} A promise that resolves when the chain execution is complete.
18
+ */
19
+ const executeChain = async (target, ammo) => {
20
+ let i = 0;
21
+
22
+ const chain = targetRegistry.globalMiddlewares.concat(
23
+ target.getMiddlewares(),
24
+ );
25
+ chain.push(target.getHandler());
26
+
27
+ const next = async () => {
28
+ const middleware = chain[i];
29
+ i++;
30
+
31
+ const args =
32
+ middleware.length === 3 ? [ammo.req, ammo.res, next] : [ammo, next];
33
+
34
+ try {
35
+ await middleware(...args);
36
+ } catch (err) {
37
+ errorHandler(ammo, err);
38
+ }
39
+ };
40
+
41
+ await next();
42
+ };
43
+
44
+ /**
45
+ * Handles errors by logging them and sending an appropriate response.
46
+ *
47
+ * @param {Ammo} ammo - The Ammo instance containing request and response objects.
48
+ * @param {Error} err - The error object to handle.
49
+ */
50
+ const errorHandler = (ammo, err) => {
51
+ if (env('LOG_EXCEPTIONS')) errorLogger.error(err);
52
+
53
+ if (err instanceof TejError) return ammo.throw(err);
54
+ return ammo.throw(err);
55
+ };
56
+
57
+ /**
58
+ * Main request handler function.
59
+ *
60
+ * @param {http.IncomingMessage} req - The HTTP request object.
61
+ * @param {http.ServerResponse} res - The HTTP response object.
62
+ * @returns {Promise<void>} A promise that resolves when the request handling is complete.
63
+ */
64
+ const handler = async (req, res) => {
65
+ const url = req.url.split('?')[0];
66
+ const target = targetRegistry.aim(url);
67
+ const ammo = new Ammo(req, res);
68
+
69
+ try {
70
+ if (target) {
71
+ await ammo.enhance();
72
+
73
+ if (env('LOG_HTTP_REQUESTS')) logHttpRequest(ammo);
74
+ await executeChain(target, ammo);
75
+ } else {
76
+ if (req.url === '/') {
77
+ ammo.defaultEntry();
78
+ } else {
79
+ errorHandler(ammo, new TejError(404, `URL not found: ${url}`));
80
+ }
81
+ }
82
+ } catch (err) {
83
+ errorHandler(ammo, err);
84
+ }
85
+ };
86
+
87
+ export default handler;
package/server/target.js CHANGED
@@ -1,52 +1,133 @@
1
+ import TejLogger from 'tej-logger';
2
+
1
3
  import isMiddlewareValid from './targets/middleware-validator.js';
2
- import TargetRegistry from './targets/registry.js';
4
+ import Endpoint from './endpoint.js';
3
5
 
6
+ import TargetRegistry from './targets/registry.js';
4
7
  const targetRegistry = new TargetRegistry();
5
8
 
6
- const isEndpointValid = (endpoint) => {
7
- if (typeof endpoint !== 'string') return false;
8
- if (endpoint.length === 0) return false;
9
- return endpoint[0] === '/';
10
- };
11
-
12
- const isShootValid = (shoot) => typeof shoot === 'function';
9
+ const logger = new TejLogger('Target');
13
10
 
11
+ /**
12
+ * Target class represents a base routing configuration for endpoints. Think of it as router in express.
13
+ * It provides functionality to set base paths, add middleware, and register endpoints.
14
+ *
15
+ * @class
16
+ * @example
17
+ * // Create a new target for user-related endpoints
18
+ * const userTarget = new Target('/user');
19
+ *
20
+ * // Add middleware that applies to all user endpoints
21
+ * userTarget.midair(authMiddleware, loggingMiddleware);
22
+ *
23
+ * // Register endpoints
24
+ * userTarget.register('/profile', (ammo) => {
25
+ * // Handle GET /user/profile
26
+ * ammo.fire({ name: 'John Doe' });
27
+ * });
28
+ *
29
+ * userTarget.register('/settings', authMiddleware, (ammo) => {
30
+ * // Handle GET /user/settings with additional auth middleware
31
+ * ammo.fire({ theme: 'dark' });
32
+ * });
33
+ */
14
34
  class Target {
35
+ /**
36
+ * Creates a new Target instance.
37
+ *
38
+ * @param {string} [base=''] - The base path for all endpoints registered under this target.
39
+ * Must start with '/' if provided.
40
+ * @example
41
+ * const apiTarget = new Target('/api');
42
+ * const userTarget = new Target('/user');
43
+ */
15
44
  constructor(base = '') {
16
45
  this.base = base;
17
46
  this.targetMiddlewares = [];
18
47
  }
19
48
 
49
+ /**
50
+ * Sets the base path for the target.
51
+ *
52
+ * @param {string} base - The base path to set. Must start with '/'.
53
+ * @returns {void}
54
+ * @example
55
+ * const target = new Target();
56
+ * target.base('/api/v1');
57
+ */
20
58
  base(base) {
21
59
  if (!base || !base.startsWith('/')) return;
22
60
  this.base = base;
23
61
  }
24
62
 
63
+ /**
64
+ * Adds middleware functions to the target.
65
+ * These middleware functions will be applied to all endpoints registered under this target.
66
+ *
67
+ * @param {...Function} middlewares - One or more middleware functions to add.
68
+ * @returns {void}
69
+ * @example
70
+ * // Add authentication middleware to all endpoints
71
+ * target.midair(authMiddleware);
72
+ *
73
+ * // Add multiple middleware functions
74
+ * target.midair(loggingMiddleware, errorHandler, rateLimiter);
75
+ */
25
76
  midair() {
26
77
  if (!arguments) return;
27
78
  const middlewares = [...arguments];
79
+
28
80
  const validMiddlewares = middlewares.filter(isMiddlewareValid);
29
81
  this.targetMiddlewares = this.targetMiddlewares.concat(validMiddlewares);
30
82
  }
31
83
 
84
+ /**
85
+ * Registers a new endpoint under this target.
86
+ *
87
+ * @param {string} path - The path for the endpoint, relative to the base path.
88
+ * @param {...Function} [middlewares] - Optional middleware functions specific to this endpoint.
89
+ * @param {Function} shoot - The handler function for the endpoint.
90
+ * @returns {void}
91
+ * @throws {Error} If there's an error registering the endpoint.
92
+ * @example
93
+ * // Register a simple endpoint
94
+ * target.register('/hello', (ammo) => {
95
+ * ammo.fire({ message: 'Hello World' });
96
+ * });
97
+ *
98
+ * // Register an endpoint with specific middleware
99
+ * target.register('/protected', authMiddleware, (ammo) => {
100
+ * ammo.fire({ data: 'Protected data' });
101
+ * });
102
+ *
103
+ * // Register an endpoint with multiple middleware
104
+ * target.register('/api/data',
105
+ * authMiddleware,
106
+ * rateLimiter,
107
+ * (ammo) => {
108
+ * ammo.fire({ data: 'Rate limited data' });
109
+ * }
110
+ * );
111
+ */
32
112
  register() {
33
113
  let args = arguments;
34
114
  if (!args) return;
35
115
 
36
- const endpoint = args[0];
37
- if (!isEndpointValid(endpoint)) return;
38
-
116
+ const path = args[0];
39
117
  const shoot = args[args.length - 1];
40
- if (!isShootValid(shoot)) return;
41
-
42
118
  const middlewares = Array.from(args).slice(1, args.length - 1);
43
- const validMiddlewares = middlewares.filter(isMiddlewareValid);
44
119
 
45
- targetRegistry.targets.push({
46
- endpoint: this.base + endpoint,
47
- middlewares: this.targetMiddlewares.concat(validMiddlewares),
48
- shoot,
49
- });
120
+ try {
121
+ const endpoint = new Endpoint();
122
+ endpoint
123
+ .setPath(this.base, path)
124
+ .setMiddlewares(middlewares)
125
+ .setHandler(shoot);
126
+
127
+ targetRegistry.targets.push(endpoint);
128
+ } catch (error) {
129
+ logger.error(`Error registering target ${path}: ${error.message}`);
130
+ }
50
131
  }
51
132
  }
52
133
 
@@ -0,0 +1,21 @@
1
+ import TejLogger from 'tej-logger';
2
+
3
+ const logger = new TejLogger('PathValidator');
4
+
5
+ const standardizePath = (path) => {
6
+ if (!path || path.length === 0) return '';
7
+
8
+ let standardized = path.startsWith('/') ? path : `/${path}`;
9
+ return standardized.endsWith('/') ? standardized.slice(0, -1) : standardized;
10
+ };
11
+
12
+ const isPathValid = (path) => {
13
+ if (typeof path !== 'string') {
14
+ logger.error(`Path ${path} should be a string. Skipping...`);
15
+ return false;
16
+ }
17
+
18
+ return true;
19
+ };
20
+
21
+ export { isPathValid, standardizePath };
@@ -32,13 +32,24 @@ class TargetRegistry {
32
32
  }
33
33
  }
34
34
 
35
- aim(method, endpoint) {
35
+ aim(endpoint) {
36
36
  return this.targets.find((target) => {
37
- return (
38
- target.endpoint === endpoint
39
- );
37
+ return target.getPath() === endpoint;
40
38
  });
41
39
  }
40
+
41
+ getAllEndpoints(grouped) {
42
+ if (grouped) {
43
+ return this.targets.reduce((acc, target) => {
44
+ const group = target.getPath().split('/')[1];
45
+ if (!acc[group]) acc[group] = [];
46
+ acc[group].push(target.getPath());
47
+ return acc;
48
+ }, {});
49
+ } else {
50
+ return this.targets.map((target) => target.getPath());
51
+ }
52
+ }
42
53
  }
43
54
 
44
55
  export default TargetRegistry;
@@ -0,0 +1,21 @@
1
+ import TejLogger from 'tej-logger';
2
+ import Ammo from '../ammo.js';
3
+ const logger = new TejLogger('ShootValidator');
4
+
5
+ const isShootValid = (shoot) => {
6
+ if (typeof shoot !== 'function') {
7
+ logger.error(`Shoot ${shoot} should be a function. Skipping...`);
8
+ return false;
9
+ }
10
+
11
+ // Shoot should have 1 parameter, and it must be an instance of Ammo
12
+ if (shoot.length !== 1) {
13
+ logger.error(`Shoot function must have 1 parameter. Skipping...`);
14
+ return false;
15
+ }
16
+
17
+
18
+ return true;
19
+ };
20
+
21
+ export default isShootValid;
package/te.js CHANGED
@@ -1,129 +1,137 @@
1
- import { createServer } from 'node:http';
2
-
3
- import { env, setEnv } from 'tej-env';
4
- import TejLogger from 'tej-logger';
5
- import database from './database/index.js';
6
-
7
- import TargetRegistry from './server/targets/registry.js';
8
-
9
- import { loadConfigFile, standardizeObj } from './utils/configuration.js';
10
-
11
- import targetHandler from './server/handler.js';
12
- import { findTargetFiles } from './utils/auto-register.js';
13
- import { pathToFileURL } from 'node:url';
14
-
15
- const logger = new TejLogger('Tejas');
16
- const targetRegistry = new TargetRegistry();
17
-
18
- class Tejas {
19
- /*
20
- * Constructor for Tejas
21
- * @param {Object} args - Arguments for Tejas
22
- * @param {Number} args.port - Port to run Tejas on
23
- * @param {Boolean} args.log.http_requests - Whether to log incoming HTTP requests
24
- * @param {Boolean} args.log.exceptions - Whether to log exceptions
25
- * @param {String} args.db.type - Database type. It can be 'mongodb', 'mysql', 'postgres', 'sqlite'
26
- * @param {String} args.db.uri - Connection URI string for the database
27
- */
28
- constructor(args) {
29
- if (Tejas.instance) return Tejas.instance;
30
- Tejas.instance = this;
31
-
32
- this.generateConfiguration(args);
33
- this.registerTargetsDir();
34
- }
35
-
36
- /*
37
- * Connect to a database
38
- * @param {Object}
39
- * @param {String} args.db - Database type. It can be 'mongodb', 'mysql', 'postgres', 'sqlite'
40
- * @param {String} args.uri - Connection URI string for the database
41
- * @param {Object} args.options - Options for the database connection
42
- */
43
- connectDatabase(args) {
44
- const db = env('DB_TYPE');
45
- const uri = env('DB_URI');
46
-
47
- if (!db) return;
48
- if (!uri) {
49
- logger.error(
50
- `Tejas could not connect to ${db} as it couldn't find a connection URI. See our documentation for more information.`,
51
- false,
52
- );
53
- return;
54
- }
55
-
56
- const connect = database[db];
57
- if (!connect) {
58
- logger.error(
59
- `Tejas could not connect to ${db} as it is not supported. See our documentation for more information.`,
60
- false,
61
- );
62
- return;
63
- }
64
-
65
- connect(uri, {}, (error) => {
66
- if (error) {
67
- logger.error(
68
- `Tejas could not connect to ${db}. Error: ${error}`,
69
- false,
70
- );
71
- return;
72
- }
73
-
74
- logger.info(`Tejas connected to ${db} successfully.`);
75
- });
76
- }
77
-
78
- generateConfiguration(options) {
79
- const configVars = standardizeObj(loadConfigFile());
80
- const envVars = standardizeObj(process.env);
81
- const userVars = standardizeObj(options);
82
-
83
- const config = { ...configVars, ...envVars, ...userVars };
84
- for (const key in config) {
85
- if (config.hasOwnProperty(key)) {
86
- setEnv(key, config[key]);
87
- }
88
- }
89
-
90
- // Load defaults
91
- if (!env('PORT')) setEnv('PORT', 1403);
92
- }
93
-
94
- midair() {
95
- if (!arguments) return;
96
- targetRegistry.addGlobalMiddleware(...arguments);
97
- }
98
-
99
- registerTargetsDir() {
100
- findTargetFiles().then((targetFiles) => {
101
- if (targetFiles) {
102
- for (const file of targetFiles) {
103
- import(pathToFileURL(`${file.path}/${file.name}`));
104
- }
105
- }
106
-
107
- }).catch((err) => {
108
- logger.error(
109
- `Tejas could not register target files. Error: ${err}`,
110
- false,
111
- );
112
- });
113
- }
114
-
115
- takeoff() {
116
- this.engine = createServer(targetHandler);
117
- this.engine.listen(env('PORT'), () => {
118
- logger.info(`Took off from port ${env('PORT')}`);
119
- this.connectDatabase();
120
- });
121
- }
122
- }
123
-
124
- export {default as Target} from './server/target.js';
125
- export {default as TejFileUploader} from './server/files/uploader.js';
126
- export default Tejas;
127
-
128
- // TODO Ability to register a target (route) from tejas instance
129
- // TODO tejas as CLI tool
1
+ import { createServer } from 'node:http';
2
+
3
+ import { env, setEnv } from 'tej-env';
4
+ import TejLogger from 'tej-logger';
5
+ import database from './database/index.js';
6
+
7
+ import TargetRegistry from './server/targets/registry.js';
8
+
9
+ import { loadConfigFile, standardizeObj } from './utils/configuration.js';
10
+
11
+ import targetHandler from './server/handler.js';
12
+ import { findTargetFiles } from './utils/auto-register.js';
13
+ import { pathToFileURL } from 'node:url';
14
+ import { log } from 'node:console';
15
+
16
+ const logger = new TejLogger('Tejas');
17
+ const targetRegistry = new TargetRegistry();
18
+
19
+ class Tejas {
20
+ /*
21
+ * Constructor for Tejas
22
+ * @param {Object} args - Arguments for Tejas
23
+ * @param {Number} args.port - Port to run Tejas on
24
+ * @param {Boolean} args.log.http_requests - Whether to log incoming HTTP requests
25
+ * @param {Boolean} args.log.exceptions - Whether to log exceptions
26
+ * @param {String} args.db.type - Database type. It can be 'mongodb', 'mysql', 'postgres', 'sqlite'
27
+ * @param {String} args.db.uri - Connection URI string for the database
28
+ */
29
+ constructor(args) {
30
+ if (Tejas.instance) return Tejas.instance;
31
+ Tejas.instance = this;
32
+
33
+ this.generateConfiguration(args);
34
+ this.registerTargetsDir();
35
+ }
36
+
37
+ /*
38
+ * Connect to a database
39
+ * @param {Object}
40
+ * @param {String} args.db - Database type. It can be 'mongodb', 'mysql', 'postgres', 'sqlite'
41
+ * @param {String} args.uri - Connection URI string for the database
42
+ * @param {Object} args.options - Options for the database connection
43
+ */
44
+ connectDatabase(args) {
45
+ const db = env('DB_TYPE');
46
+ const uri = env('DB_URI');
47
+
48
+ if (!db) return;
49
+ if (!uri) {
50
+ logger.error(
51
+ `Tejas could not connect to ${db} as it couldn't find a connection URI. See our documentation for more information.`,
52
+ false,
53
+ );
54
+ return;
55
+ }
56
+
57
+ const connect = database[db];
58
+ if (!connect) {
59
+ logger.error(
60
+ `Tejas could not connect to ${db} as it is not supported. See our documentation for more information.`,
61
+ false,
62
+ );
63
+ return;
64
+ }
65
+
66
+ connect(uri, {}, (error) => {
67
+ if (error) {
68
+ logger.error(
69
+ `Tejas could not connect to ${db}. Error: ${error}`,
70
+ false,
71
+ );
72
+ return;
73
+ }
74
+
75
+ logger.info(`Tejas connected to ${db} successfully.`);
76
+ });
77
+ }
78
+
79
+ generateConfiguration(options) {
80
+ const configVars = standardizeObj(loadConfigFile());
81
+ const envVars = standardizeObj(process.env);
82
+ const userVars = standardizeObj(options);
83
+
84
+ const config = { ...configVars, ...envVars, ...userVars };
85
+ for (const key in config) {
86
+ if (config.hasOwnProperty(key)) {
87
+ setEnv(key, config[key]);
88
+ }
89
+ }
90
+
91
+ // Load defaults
92
+ if (!env('PORT')) setEnv('PORT', 1403);
93
+ }
94
+
95
+ midair() {
96
+ if (!arguments) return;
97
+ targetRegistry.addGlobalMiddleware(...arguments);
98
+ }
99
+
100
+ registerTargetsDir() {
101
+ findTargetFiles()
102
+ .then((targetFiles) => {
103
+ if (targetFiles) {
104
+ for (const file of targetFiles) {
105
+ import(pathToFileURL(`${file.parentPath}/${file.name}`));
106
+ }
107
+ }
108
+ })
109
+ .catch((err) => {
110
+ logger.error(
111
+ `Tejas could not register target files. Error: ${err}`,
112
+ false,
113
+ );
114
+ });
115
+ }
116
+
117
+ takeoff() {
118
+ this.engine = createServer(targetHandler);
119
+ this.engine.listen(env('PORT'), () => {
120
+ logger.info(`Took off from port ${env('PORT')}`);
121
+ this.connectDatabase();
122
+ });
123
+ }
124
+ }
125
+
126
+ const listAllEndpoints = (grouped = false) => {
127
+ return targetRegistry.getAllEndpoints(grouped);
128
+ };
129
+
130
+ export { default as Target } from './server/target.js';
131
+ export { default as TejFileUploader } from './server/files/uploader.js';
132
+ export { default as TejError } from './server/error.js';
133
+ export { listAllEndpoints };
134
+ export default Tejas;
135
+
136
+ // TODO Ability to register a target (route) from tejas instance
137
+ // TODO tejas as CLI tool