soajs.controller 4.3.0 → 4.3.2

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.
@@ -6,9 +6,14 @@ on:
6
6
  pull_request:
7
7
  branches: [ master, develop ]
8
8
 
9
+ env:
10
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
11
+
9
12
  jobs:
10
13
  test:
11
14
  runs-on: ubuntu-latest
15
+ env:
16
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
12
17
 
13
18
  services:
14
19
  mongodb:
@@ -18,10 +23,10 @@ jobs:
18
23
 
19
24
  steps:
20
25
  - name: Checkout code
21
- uses: actions/checkout@v4
26
+ uses: actions/checkout@v4.2.2
22
27
 
23
28
  - name: Setup Node.js
24
- uses: actions/setup-node@v4
29
+ uses: actions/setup-node@v4.4.0
25
30
  with:
26
31
  node-version: '24'
27
32
 
@@ -6,9 +6,14 @@ on:
6
6
  - release
7
7
  - 'release/**'
8
8
 
9
+ env:
10
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
11
+
9
12
  jobs:
10
13
  test:
11
14
  runs-on: ubuntu-latest
15
+ env:
16
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
12
17
 
13
18
  services:
14
19
  mongodb:
@@ -18,10 +23,10 @@ jobs:
18
23
 
19
24
  steps:
20
25
  - name: Checkout code
21
- uses: actions/checkout@v4
26
+ uses: actions/checkout@v4.2.2
22
27
 
23
28
  - name: Setup Node.js
24
- uses: actions/setup-node@v4
29
+ uses: actions/setup-node@v4.4.0
25
30
  with:
26
31
  node-version: '24'
27
32
 
@@ -40,10 +45,12 @@ jobs:
40
45
  publish-docker:
41
46
  runs-on: ubuntu-latest
42
47
  needs: test
48
+ env:
49
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
43
50
 
44
51
  steps:
45
52
  - name: Checkout code
46
- uses: actions/checkout@v4
53
+ uses: actions/checkout@v4.2.2
47
54
 
48
55
  - name: Extract version from package.json
49
56
  id: version
@@ -85,7 +92,7 @@ jobs:
85
92
  run: git push origin ${{ steps.version.outputs.PACKAGE_VERSION }} || true
86
93
 
87
94
  - name: Create GitHub Release
88
- uses: softprops/action-gh-release@v1
95
+ uses: softprops/action-gh-release@v2
89
96
  with:
90
97
  tag_name: ${{ steps.version.outputs.PACKAGE_VERSION }}
91
98
  name: Release ${{ steps.version.outputs.PACKAGE_VERSION }}
@@ -12,6 +12,121 @@ const get = (p, o) => p.reduce((xs, x) => (xs && xs[x]) ? xs[x] : null, o);
12
12
 
13
13
  const { httpRequestLight, proxyRequestMonitor } = require("../../lib/request.js");
14
14
 
15
+ // Monitor buffer for batch uploads
16
+ let monitorBuffer = [];
17
+ let monitorFlushTimer = null;
18
+ let monitorFlushInProgress = false;
19
+ let lastReqForFlush = null;
20
+
21
+ /**
22
+ * Flush the monitor buffer to soamonitor
23
+ */
24
+ function flushMonitorBuffer() {
25
+ if (monitorFlushInProgress || monitorBuffer.length === 0 || !lastReqForFlush) {
26
+ return;
27
+ }
28
+
29
+ monitorFlushInProgress = true;
30
+
31
+ // Clear timer if exists
32
+ if (monitorFlushTimer) {
33
+ clearTimeout(monitorFlushTimer);
34
+ monitorFlushTimer = null;
35
+ }
36
+
37
+ // Take current buffer and reset
38
+ let itemsToSend = monitorBuffer;
39
+ let req = lastReqForFlush;
40
+ monitorBuffer = [];
41
+
42
+ let soamonitor = "soamonitor";
43
+ let soamonitorVersion = "1";
44
+ let port = get(["registry", "services", "soamonitor", "port"], req.soajs) || "4050";
45
+ let api = "/monitor/items"; // Batch endpoint
46
+
47
+ req.soajs.awareness.getHost(soamonitor, soamonitorVersion, function (host) {
48
+ monitorFlushInProgress = false;
49
+
50
+ if (!host) {
51
+ req.soajs.log.debug('Monitor buffer flush: Unable to find host for ' + soamonitor);
52
+ // Put items back in buffer for retry
53
+ monitorBuffer = itemsToSend.concat(monitorBuffer);
54
+ return;
55
+ }
56
+
57
+ // Convert body/response to strings
58
+ itemsToSend.forEach(doc => {
59
+ if (doc.body) {
60
+ doc.body = doc.body.toString();
61
+ }
62
+ if (doc.response) {
63
+ doc.response = doc.response.toString();
64
+ }
65
+ });
66
+
67
+ let uri = "http://" + host + ":" + port + api;
68
+ let options = {
69
+ "method": "POST",
70
+ "uri": uri,
71
+ "data": { "items": itemsToSend },
72
+ "json": true
73
+ };
74
+
75
+ httpRequestLight(options)
76
+ .then((body) => {
77
+ if (body && !body.result) {
78
+ req.soajs.log.debug('Monitor buffer flush response:', body);
79
+ } else {
80
+ req.soajs.log.debug('Monitor buffer flushed: ' + itemsToSend.length + ' items');
81
+ }
82
+ })
83
+ .catch((error) => {
84
+ req.soajs.log.debug('Monitor buffer flush failed: ' + error.message);
85
+ });
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Add item to monitor buffer and trigger flush if needed
91
+ * @param {Object} req - Request object
92
+ * @param {Object} doc - Monitor document to buffer
93
+ * @param {Object} monitor - Monitor configuration
94
+ */
95
+ function bufferMonitorItem(req, doc, monitor) {
96
+ monitorBuffer.push(doc);
97
+ lastReqForFlush = req;
98
+
99
+ let bufferConfig = monitor.buffer || {};
100
+ let bufferTime = bufferConfig.time;
101
+ let bufferLimit = bufferConfig.limit;
102
+
103
+ // Check count threshold
104
+ if (bufferLimit && monitorBuffer.length >= bufferLimit) {
105
+ flushMonitorBuffer();
106
+ return;
107
+ }
108
+
109
+ // Start timer if not already running and time threshold is set
110
+ if (bufferTime && !monitorFlushTimer) {
111
+ monitorFlushTimer = setTimeout(() => {
112
+ monitorFlushTimer = null;
113
+ flushMonitorBuffer();
114
+ }, bufferTime);
115
+ }
116
+ }
117
+
118
+ // Graceful shutdown - flush remaining buffer
119
+ process.on('SIGTERM', () => {
120
+ if (monitorBuffer.length > 0 && !monitorFlushInProgress) {
121
+ // Best-effort cleanup
122
+ monitorBuffer = [];
123
+ if (monitorFlushTimer) {
124
+ clearTimeout(monitorFlushTimer);
125
+ monitorFlushTimer = null;
126
+ }
127
+ }
128
+ });
129
+
15
130
  module.exports = (configuration) => {
16
131
 
17
132
  let core = configuration.core;
@@ -26,12 +141,21 @@ module.exports = (configuration) => {
26
141
  */
27
142
  return (req, res) => {
28
143
  preRedirect(req, res, core, function (obj) {
144
+ let monitor = get(["registry", "custom", "gateway", "value", "gotoService", "monitor"], req.soajs);
29
145
  let log_monitor_triggered = false;
30
146
  let log_monitor = (doc) => {
31
147
  if (log_monitor_triggered) {
32
148
  return;
33
149
  }
34
150
  log_monitor_triggered = true;
151
+
152
+ // Check if buffering is enabled
153
+ if (monitor && monitor.buffer && (monitor.buffer.time || monitor.buffer.limit)) {
154
+ bufferMonitorItem(req, doc, monitor);
155
+ return;
156
+ }
157
+
158
+ // Original immediate send logic (unchanged)
35
159
  let soamonitor = "soamonitor";
36
160
  let soamonitorVersion = "1";
37
161
  let port = get(["registry", "services", "soamonitor", "port"], req.soajs);
@@ -72,8 +196,6 @@ module.exports = (configuration) => {
72
196
  }
73
197
  });
74
198
  };
75
-
76
- let monitor = get(["registry", "custom", "gateway", "value", "gotoService", "monitor"], req.soajs);
77
199
  let monitoObj = {
78
200
  "time": {}
79
201
  };
@@ -9,8 +9,131 @@
9
9
  */
10
10
 
11
11
  const { httpRequest } = require("../../lib/request.js");
12
+ const { pathToRegexp } = require("path-to-regexp");
12
13
  const get = (p, o) => p.reduce((xs, x) => (xs && xs[x]) ? xs[x] : null, o);
13
14
 
15
+ // Cache compiled RegExp objects for route pattern matching
16
+ const regexpCache = new Map();
17
+ const MAX_REGEXP_CACHE_SIZE = 100;
18
+
19
+ /**
20
+ * Check if a route pattern contains path parameters
21
+ * @param {string} route - The route pattern
22
+ * @returns {boolean}
23
+ */
24
+ function hasPathParams(route) {
25
+ return route.includes("/:");
26
+ }
27
+
28
+ /**
29
+ * Get or create a cached RegExp for a route pattern
30
+ * @param {string} route - The route pattern
31
+ * @returns {RegExp}
32
+ */
33
+ function getRouteRegExp(route) {
34
+ if (regexpCache.has(route)) {
35
+ return regexpCache.get(route);
36
+ }
37
+
38
+ let out = pathToRegexp(route, { sensitive: true });
39
+ let regexp = out.regexp || out;
40
+
41
+ // Simple LRU: clear oldest entries when cache is full
42
+ if (regexpCache.size >= MAX_REGEXP_CACHE_SIZE) {
43
+ const keysToDelete = Array.from(regexpCache.keys()).slice(0, Math.floor(MAX_REGEXP_CACHE_SIZE * 0.25));
44
+ keysToDelete.forEach(key => regexpCache.delete(key));
45
+ }
46
+
47
+ regexpCache.set(route, regexp);
48
+ return regexp;
49
+ }
50
+
51
+ /**
52
+ * Check if the current request should trigger lastSeen
53
+ * @param {Object} include - The include whitelist configuration
54
+ * @param {string} serviceName - The current service name
55
+ * @param {string} apiPath - The current API path
56
+ * @param {string} method - The HTTP method (GET, POST, etc.)
57
+ * @returns {boolean} - true if lastSeen should be triggered
58
+ */
59
+ function shouldTriggerLastSeen(include, serviceName, apiPath, method) {
60
+ // No include filter = trigger for all (backward compatible)
61
+ if (!include) {
62
+ return true;
63
+ }
64
+
65
+ // Service not in whitelist
66
+ if (!include[serviceName]) {
67
+ return false;
68
+ }
69
+
70
+ const serviceConfig = include[serviceName];
71
+
72
+ // Service-level match (all APIs, all methods)
73
+ if (serviceConfig === true) {
74
+ return true;
75
+ }
76
+
77
+ // API-level match
78
+ if (typeof serviceConfig === 'object' && serviceConfig.apis) {
79
+ return isApiMethodMatch(serviceConfig.apis, apiPath, method);
80
+ }
81
+
82
+ return false;
83
+ }
84
+
85
+ /**
86
+ * Check if API path and method match the apis configuration
87
+ * @param {Object} apis - The apis configuration object
88
+ * @param {string} apiPath - The current API path
89
+ * @param {string} method - The HTTP method
90
+ * @returns {boolean}
91
+ */
92
+ function isApiMethodMatch(apis, apiPath, method) {
93
+ // Check for wildcard first
94
+ if (apis["*"]) {
95
+ return isMethodMatch(apis["*"], method);
96
+ }
97
+
98
+ // Check exact match first
99
+ if (apis[apiPath]) {
100
+ return isMethodMatch(apis[apiPath], method);
101
+ }
102
+
103
+ // Check pattern matching for routes with path params
104
+ for (const routePattern of Object.keys(apis)) {
105
+ if (hasPathParams(routePattern)) {
106
+ const regexp = getRouteRegExp(routePattern);
107
+ if (regexp.test(apiPath)) {
108
+ return isMethodMatch(apis[routePattern], method);
109
+ }
110
+ }
111
+ }
112
+
113
+ return false;
114
+ }
115
+
116
+ /**
117
+ * Check if method matches the method configuration
118
+ * @param {boolean|Array} methodConfig - true for all methods, or array of methods
119
+ * @param {string} method - The HTTP method
120
+ * @returns {boolean}
121
+ */
122
+ function isMethodMatch(methodConfig, method) {
123
+ // true = all methods
124
+ if (methodConfig === true) {
125
+ return true;
126
+ }
127
+
128
+ // Array of methods (case-insensitive)
129
+ if (Array.isArray(methodConfig)) {
130
+ const lowerMethod = method.toLowerCase();
131
+ return methodConfig.some(m => m.toLowerCase() === lowerMethod);
132
+ }
133
+
134
+ return false;
135
+ }
136
+
14
137
  /**
15
138
  *
16
139
  * @param configuration
@@ -25,6 +148,16 @@ module.exports = () => {
25
148
  let lastSeen = get(["soajs", "registry", "custom", "gateway", "value", "lastSeen"], req);
26
149
  if (lastSeen && lastSeen.active) {
27
150
 
151
+ // Check include filter
152
+ let currentService = get(["soajs", "controller", "serviceParams", "name"], req);
153
+ let currentApi = get(["soajs", "controller", "serviceParams", "parsedUrl", "pathname"], req);
154
+ let currentMethod = req.method;
155
+
156
+ if (!shouldTriggerLastSeen(lastSeen.include, currentService, currentApi, currentMethod)) {
157
+ req.soajs.log.debug("lastSeen skipped for [" + currentService + " " + currentMethod + " " + currentApi + "]");
158
+ return;
159
+ }
160
+
28
161
  const item = {
29
162
  "name": lastSeen.serviceName || "urac",
30
163
  "version": lastSeen.serviceVersion || "3",
@@ -62,49 +195,7 @@ module.exports = () => {
62
195
  } else {
63
196
  req.soajs.log.debug("lastSeen ", body);
64
197
  }
65
- // else if (!body.result) {
66
- // let errorMsg = "";
67
- // if (body.errors && body.errors && body.errors.details && body.errors.details.length > 0) {
68
- // body.errors.details.forEach((detail) => {
69
- // if (errorMsg === "") {
70
- // errorMsg += detail.message;
71
- // }
72
- // else {
73
- // errorMsg += " - " + detail.message;
74
- // }
75
- // });
76
- // }
77
- // req.soajs.log.debug("lastSeen", errorMsg);
78
- // }
79
198
  });
80
-
81
- // let options = {
82
- // method: "post",
83
- // uri: "http://" + item.host + ":" + item.port + item.api,
84
- // json: true
85
- // };
86
- // if (req.headers && req.headers.soajsinjectobj) {
87
- // options.headers = { "soajsinjectobj": req.headers.soajsinjectobj };
88
- // }
89
- // request(options, (error, response, body) => {
90
- // if (error) {
91
- // req.soajs.log.debug("lastSeen failed for [" + item.name + "@" + item.version + " " + item.api + "]");
92
- // req.soajs.log.debug("lastSeen " + error.message);
93
- // } else if (!body.result) {
94
- // let errorMsg = "";
95
- // if (body.errors && body.errors && body.errors.details && body.errors.details.length > 0) {
96
- // body.errors.details.forEach((detail) => {
97
- // if (errorMsg === "") {
98
- // errorMsg += detail.message;
99
- // }
100
- // else {
101
- // errorMsg += " - " + detail.message;
102
- // }
103
- // });
104
- // }
105
- // req.soajs.log.debug("lastSeen", errorMsg);
106
- // }
107
- // });
108
199
  } else {
109
200
  req.soajs.log.debug("lastSeen failed with no host for [" + item.name + "@" + item.version + " " + item.api + "]");
110
201
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "soajs.controller",
3
3
  "description": "soajs multi tenant API gateway",
4
- "version": "4.3.0",
4
+ "version": "4.3.2",
5
5
  "author": {
6
6
  "name": "soajs team",
7
7
  "email": "team@soajs.org"