@triophore/falconjs 1.0.1 → 1.0.4

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/falcon.js CHANGED
@@ -469,49 +469,74 @@ class Falcon {
469
469
  * @param {string[]} files - Array to collect file paths (used for recursion)
470
470
  * @returns {Promise<string[]>} Array of JavaScript file paths
471
471
  */
472
- async #findJsFilesRecursively(dir, files = []) {
473
- const items = await fs.promises.readdir(dir, { withFileTypes: true });
472
+ async #findJsFilesRecursively(dir) {
473
+ let files = [];
474
+ try {
475
+ const items = await fs.promises.readdir(dir, { withFileTypes: true });
474
476
 
475
- for (const item of items) {
476
- const fullPath = path.join(dir, item.name);
477
+ for (const item of items) {
478
+ const fullPath = path.join(dir, item.name);
477
479
 
478
- if (item.isDirectory() && !item.name.startsWith('.')) {
479
- await this.#findJsFilesRecursively(fullPath, files);
480
- }
481
- else if (item.isFile() && item.name.endsWith('.js') && !item.name.endsWith('.test.js')) {
482
- files.push(fullPath);
480
+ if (item.isDirectory() && !item.name.startsWith('.')) {
481
+ // Recursively find files and concat
482
+ const nestedFiles = await this.#findJsFilesRecursively(fullPath);
483
+ files = files.concat(nestedFiles);
484
+ }
485
+ else if (item.isFile() && item.name.endsWith('.js') && !item.name.endsWith('.test.js')) {
486
+ files.push(fullPath);
487
+ }
483
488
  }
489
+ } catch (err) {
490
+ this.CONTEXT["logger"].error(`Error scanning directory ${dir}:`, err.message);
484
491
  }
485
492
 
486
493
  return files;
487
494
  }
488
495
 
489
496
  /**
490
- * Builds a clean route path from file system path.
491
- * Converts folder structure to REST-style paths with parameter support.
497
+ * Parses route file path to extract method and route path.
498
+ * Validates against [METHOD]#filename.js pattern.
492
499
  *
493
500
  * @private
494
501
  * @param {string} relativePath - Relative path from routes directory
495
- * @returns {string} Clean route path
502
+ * @returns {Object|null} Object { method, path } or null if invalid
496
503
  */
497
- #buildRoutePath(relativePath) {
498
- let routePath = '/' + path.dirname(relativePath)
499
- .replace(/\\/g, '/')
500
- .replace(/\/index$/gi, '')
501
- .replace(/\/_/g, '/:')
502
- .replace(/_/g, '-')
503
- .replace(/(^|\/)\[([^\]]+)\]/g, '$1:$2');
504
-
505
- const baseName = path.basename(relativePath, '.js');
506
- if (baseName !== 'index') {
507
- let segment = baseName
508
- .replace(/^_/, ':')
509
- .replace(/_/g, '-')
510
- .replace(/^\[([^\]]+)\]$/, ':$1');
511
- routePath = path.posix.join(routePath, segment);
504
+ #parseRouteFile(relativePath) {
505
+ const filename = path.basename(relativePath);
506
+
507
+ // Check for [METHOD]# pattern
508
+ // Allows any methods characters, will be validated by Hapi or logic if needed, but user said ANY is valid
509
+ const match = filename.match(/^\[([A-Z]+)\]#(.*)\.js$/i);
510
+ if (!match) {
511
+ return null;
512
512
  }
513
513
 
514
- return routePath.replace(/\/+$/, '') || '/';
514
+ const method = match[1].toUpperCase();
515
+ const rawName = match[2];
516
+
517
+ let dirname = path.dirname(relativePath);
518
+ if (dirname === '.') dirname = '';
519
+
520
+ // Convert dirname params [param] -> {param}
521
+ let routePath = '/' + dirname
522
+ .split('/')
523
+ .map(segment => segment.replace(/^\[(.+)\]$/, '{$1}'))
524
+ .join('/');
525
+
526
+ // Clean up path
527
+ routePath = routePath.replace(/\/+/g, '/');
528
+
529
+ // Append filename (unless index)
530
+ if (rawName !== 'index') {
531
+ routePath = path.posix.join(routePath, rawName);
532
+ }
533
+
534
+ // Ensure no trailing slash (unless root)
535
+ if (routePath !== '/') {
536
+ routePath = routePath.replace(/\/$/, '');
537
+ }
538
+
539
+ return { method, path: routePath };
515
540
  }
516
541
 
517
542
  /**
@@ -526,12 +551,15 @@ class Falcon {
526
551
  #findBestValidatorName(baseName, relativePath, validators) {
527
552
  const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
528
553
 
554
+ // Adjust baseName for validator search since we stripped [METHOD]#
555
+ const realBaseName = baseName.replace(/^\[[A-Z]+\]#/, '');
556
+
529
557
  const candidates = [
530
- baseName,
531
- baseName + 'Payload',
532
- path.dirname(relativePath).split(path.sep).pop() + baseName,
533
- capitalize(baseName),
534
- baseName.replace(/Handler$/i, ''),
558
+ realBaseName,
559
+ realBaseName + 'Payload',
560
+ path.dirname(relativePath).split(path.sep).pop() + realBaseName,
561
+ capitalize(realBaseName),
562
+ realBaseName.replace(/Handler$/i, ''),
535
563
  ];
536
564
  for (const c of candidates) {
537
565
  if (validators[c]) return c;
@@ -585,8 +613,8 @@ class Falcon {
585
613
  }
586
614
 
587
615
  /**
588
- * Loads routes recursively from the routes directory with automatic validation and Swagger documentation.
589
- * Supports nested folder structures and automatic validator matching.
616
+ * Loads routes recursively from the routes directory using dynamic file naming conventions.
617
+ * Pattern: [METHOD]#filename.js
590
618
  *
591
619
  * @private
592
620
  * @async
@@ -619,45 +647,73 @@ class Falcon {
619
647
  // Register Swagger first (needs to be before routes)
620
648
  await this.#registerSwagger();
621
649
 
622
- this.CONTEXT["logger"].info("Loading routes with validation + Swagger docs...");
650
+ this.CONTEXT["logger"].info("Loading routes with dynamic naming convention...");
623
651
 
624
652
  const routeFiles = await this.#findJsFilesRecursively(routesDir);
625
653
 
626
654
  for (const filePath of routeFiles) {
627
655
  const relativePath = path.relative(routesDir, filePath);
628
- const baseName = path.basename(filePath, '.js');
629
- let routePath = this.#buildRoutePath(relativePath);
630
- this.CONTEXT["logger"].info("Route Name : " + routePath);
656
+
657
+ const parsed = this.#parseRouteFile(relativePath);
658
+ if (!parsed) {
659
+ // Discard if not matching pattern
660
+ continue;
661
+ }
662
+
663
+ const { method, path: routePath } = parsed;
664
+ this.CONTEXT["logger"].info(`Route found: [${method}] ${routePath} (${relativePath})`);
631
665
 
632
666
  try {
633
667
  const routeModule = require(filePath);
634
- const register = typeof routeModule === 'function' ? routeModule : routeModule.route?.bind(routeModule);
635
668
 
636
- if (typeof register !== 'function') continue;
669
+ // Flexible export: can be a handler function, or an object with handler/options
670
+ // OR legacy support: function that takes context (but we need to manually register)
637
671
 
638
- // Find validator + extract schema
639
- const validatorName = this.#findBestValidatorName(baseName, relativePath, validators);
640
- const validator = validatorName ? validators[validatorName] : null;
672
+ let handler, options = {};
673
+
674
+ if (typeof routeModule === 'function') {
675
+ // Assume it's a handler(request, h)
676
+ handler = routeModule;
677
+ } else if (typeof routeModule === 'object') {
678
+ if (routeModule.handler) handler = routeModule.handler;
679
+ if (routeModule.options) options = routeModule.options;
680
+
681
+ // If user exported .route function (legacy), we can't really use it easily with new params
682
+ // unless we assume they migrated. We'll stick to handler/options pattern for new files.
683
+ }
684
+
685
+ if (!handler) {
686
+ this.CONTEXT["logger"].warn(`Skipping ${relativePath}: No handler exported`);
687
+ continue;
688
+ }
641
689
 
642
690
  // Auto-generate tag from folder
643
691
  const tag = path.dirname(relativePath).split(path.sep)[0] || 'api';
644
692
 
645
- const wrappedRoute = async (server, ctx) => {
646
- // Add auto-generated route info to context
647
- ctx.route = {
648
- path: routePath,
649
- tag: tag,
650
- validator: validator,
651
- validatorName: validatorName
652
- };
653
-
654
- await register(ctx); // Only pass context, server is already in ctx.server
655
- };
693
+ // Find validator attempt
694
+ const baseName = path.basename(filePath, '.js');
695
+ const validatorName = this.#findBestValidatorName(baseName, relativePath, validators);
696
+ if (validatorName && validators[validatorName]) {
697
+ // Merge into options.validate if not present
698
+ options.validate = options.validate || {};
699
+ if (!options.validate.payload && !options.validate.query && !options.validate.params) {
700
+ // Heuristic: default to payload for POST/PUT/PATCH, query for others?
701
+ // Or just user provided. For now let's leave flexible or auto-attach if user wants.
702
+ // The old logic attached it to ctx.route.validator.
703
+ }
704
+ }
656
705
 
657
- await wrappedRoute(this.httpServer, this.CONTEXT);
706
+ // Register the route
707
+ this.httpServer.route({
708
+ method: method,
709
+ path: routePath,
710
+ handler: handler,
711
+ options: {
712
+ tags: [tag],
713
+ ...options
714
+ }
715
+ });
658
716
 
659
- const valMark = validator ? 'Validated + Documented' : 'Documented';
660
- this.CONTEXT["logger"].info(`${valMark} ${routePath} ← ${relativePath}`);
661
717
  } catch (err) {
662
718
  this.CONTEXT["logger"].error(`Route failed: ${filePath}`, err.message);
663
719
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triophore/falconjs",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "description": "simple server framework for nodejs",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,63 @@
1
+ const path = require('path');
2
+
3
+ function parseRoute(relativePath) {
4
+ const filename = path.basename(relativePath);
5
+
6
+ // 1. Check for [METHOD]# pattern
7
+ const match = filename.match(/^\[([A-Z]+)\]#(.*)\.js$/i);
8
+ if (!match) {
9
+ return null; // Ignore file
10
+ }
11
+
12
+ const method = match[1].toUpperCase();
13
+ const rawName = match[2]; // filename without [METHOD]# and .js
14
+
15
+ // 2. Build Path
16
+ let dirname = path.dirname(relativePath);
17
+ if (dirname === '.') dirname = '';
18
+
19
+ // Convert dirname params [param] -> {param}
20
+ let routePath = '/' + dirname
21
+ .split('/')
22
+ .map(segment => segment.replace(/^\[(.+)\]$/, '{$1}')) // [id] -> {id}
23
+ .join('/');
24
+
25
+ // Clean up path
26
+ routePath = routePath.replace(/\/+/g, '/');
27
+
28
+ // Append filename (unless index)
29
+ if (rawName !== 'index') {
30
+ routePath = path.posix.join(routePath, rawName);
31
+ }
32
+
33
+ // Ensure no trailing slash (unless root)
34
+ if (routePath !== '/') {
35
+ routePath = routePath.replace(/\/$/, '');
36
+ }
37
+
38
+ return {
39
+ method,
40
+ path: routePath
41
+ };
42
+ }
43
+
44
+ const tests = [
45
+ 'files.js', // Should be ignored
46
+ '[GET]#file.js', // -> /file, GET
47
+ '[POST]#index.js', // -> /, POST
48
+ 'folder_name/[GET]#file.js', // -> /folder_name/file, GET
49
+ 'folder_name/[param]/[PUT]#file.js', // -> /folder_name/{param}/file, PUT
50
+ 'users/[id]/[GET]#details.js', // -> /users/{id}/details, GET
51
+ 'users/[id]/[GET]#index.js', // -> /users/{id}, GET
52
+ 'api/v1/[DELETE]#user.js' // -> /api/v1/user, DELETE
53
+ ];
54
+
55
+ console.log('--- Testing Route Parsing ---');
56
+ tests.forEach(t => {
57
+ const result = parseRoute(t);
58
+ if (result) {
59
+ console.log(`${t} -> ${result.method} ${result.path}`);
60
+ } else {
61
+ console.log(`${t} -> IGNORED`);
62
+ }
63
+ });
@@ -0,0 +1 @@
1
+ module.exports = {}
@@ -0,0 +1 @@
1
+ module.exports = {}