@usageflow/express 0.1.9 → 0.2.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/README.md +187 -30
- package/dist/plugin.js +7 -4
- package/dist/plugin.js.map +1 -1
- package/jest.config.js +14 -0
- package/package.json +5 -6
- package/src/plugin.ts +10 -3
- package/test/plugin.test.ts +201 -0
- package/tsconfig.json +1 -0
- package/tsconfig.test.json +10 -0
package/README.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# @usageflow/express
|
|
2
2
|
|
|
3
|
-
Express.js middleware for UsageFlow API tracking. Easily monitor and analyze your Express.js API usage.
|
|
3
|
+
Express.js middleware for UsageFlow API tracking. Easily monitor and analyze your Express.js API usage with real-time tracking and allocation management.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@usageflow/express)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
7
|
|
|
7
8
|
## Installation
|
|
8
9
|
|
|
@@ -13,78 +14,234 @@ npm install @usageflow/express
|
|
|
13
14
|
## Quick Start
|
|
14
15
|
|
|
15
16
|
```javascript
|
|
16
|
-
const express = require(
|
|
17
|
-
const { ExpressUsageFlowAPI } = require(
|
|
17
|
+
const express = require('express');
|
|
18
|
+
const { ExpressUsageFlowAPI } = require('@usageflow/express');
|
|
18
19
|
|
|
19
20
|
const app = express();
|
|
20
21
|
app.use(express.json());
|
|
21
22
|
|
|
22
|
-
// Initialize UsageFlow
|
|
23
|
-
const usageFlow = new ExpressUsageFlowAPI(
|
|
24
|
-
|
|
23
|
+
// Initialize UsageFlow with API key and optional pool size
|
|
24
|
+
const usageFlow = new ExpressUsageFlowAPI({
|
|
25
|
+
apiKey: 'YOUR_API_KEY',
|
|
26
|
+
poolSize: 5, // Optional: Number of WebSocket connections (default: 5)
|
|
27
|
+
});
|
|
25
28
|
|
|
26
29
|
// Create middleware
|
|
27
30
|
const middleware = usageFlow.createMiddleware(
|
|
28
31
|
[
|
|
29
|
-
{ method:
|
|
32
|
+
{ method: '*', url: '*' }, // Track all routes
|
|
30
33
|
],
|
|
31
34
|
[
|
|
32
|
-
{ method:
|
|
33
|
-
]
|
|
35
|
+
{ method: 'GET', url: '/api/health' }, // Whitelist health check
|
|
36
|
+
]
|
|
34
37
|
);
|
|
35
38
|
|
|
36
39
|
// Apply middleware
|
|
37
40
|
app.use(middleware);
|
|
38
41
|
|
|
39
42
|
// Your routes
|
|
40
|
-
app.get(
|
|
41
|
-
res.json({ users: [
|
|
43
|
+
app.get('/api/users', (req, res) => {
|
|
44
|
+
res.json({ users: ['John', 'Jane'] });
|
|
42
45
|
});
|
|
43
46
|
|
|
44
47
|
app.listen(3000, () => {
|
|
45
|
-
console.log(
|
|
48
|
+
console.log('Server running on port 3000');
|
|
46
49
|
});
|
|
47
50
|
```
|
|
48
51
|
|
|
49
52
|
## TypeScript Support
|
|
50
53
|
|
|
51
54
|
```typescript
|
|
52
|
-
import express from
|
|
53
|
-
import { ExpressUsageFlowAPI } from
|
|
55
|
+
import express from 'express';
|
|
56
|
+
import { ExpressUsageFlowAPI } from '@usageflow/express';
|
|
54
57
|
|
|
55
58
|
const app = express();
|
|
56
59
|
app.use(express.json());
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
usageFlow
|
|
61
|
+
// Initialize UsageFlow with API key and optional pool size
|
|
62
|
+
const usageFlow = new ExpressUsageFlowAPI({
|
|
63
|
+
apiKey: 'YOUR_API_KEY',
|
|
64
|
+
poolSize: 5, // Optional: Number of WebSocket connections (default: 5)
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Create middleware
|
|
68
|
+
const middleware = usageFlow.createMiddleware(
|
|
69
|
+
[
|
|
70
|
+
{ method: '*', url: '*' }, // Track all routes
|
|
71
|
+
],
|
|
72
|
+
[
|
|
73
|
+
{ method: 'GET', url: '/api/health' }, // Whitelist health check
|
|
74
|
+
]
|
|
75
|
+
);
|
|
60
76
|
|
|
61
|
-
//
|
|
77
|
+
// Apply middleware
|
|
78
|
+
app.use(middleware);
|
|
79
|
+
|
|
80
|
+
// Your routes
|
|
81
|
+
app.get('/api/users', (req, res) => {
|
|
82
|
+
res.json({ users: ['John', 'Jane'] });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
app.listen(3000, () => {
|
|
86
|
+
console.log('Server running on port 3000');
|
|
87
|
+
});
|
|
62
88
|
```
|
|
63
89
|
|
|
64
|
-
## Configuration
|
|
90
|
+
## Configuration
|
|
91
|
+
|
|
92
|
+
### ExpressUsageFlowAPI
|
|
93
|
+
|
|
94
|
+
#### Constructor Options
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
interface UsageFlowAPIConfig {
|
|
98
|
+
apiKey: string; // Your UsageFlow API key (required)
|
|
99
|
+
poolSize?: number; // Number of WebSocket connections (default: 5)
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### createMiddleware
|
|
104
|
+
|
|
105
|
+
Creates Express middleware for tracking API usage.
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
createMiddleware(
|
|
109
|
+
routes: Route[], // Routes to track
|
|
110
|
+
whitelistRoutes?: Route[] // Routes to exclude from tracking
|
|
111
|
+
): (req: Request, res: Response, next: NextFunction) => Promise<void>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### Route Configuration
|
|
65
115
|
|
|
66
116
|
```typescript
|
|
67
117
|
interface Route {
|
|
68
|
-
|
|
69
|
-
|
|
118
|
+
method: string; // HTTP method ('GET', 'POST', 'PUT', 'DELETE', etc.) or '*' for all methods
|
|
119
|
+
url: string; // URL pattern or '*' for all URLs
|
|
70
120
|
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### Examples
|
|
124
|
+
|
|
125
|
+
**Track all routes:**
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
const middleware = usageFlow.createMiddleware([{ method: '*', url: '*' }]);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Track specific routes:**
|
|
71
132
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
133
|
+
```typescript
|
|
134
|
+
const middleware = usageFlow.createMiddleware([
|
|
135
|
+
{ method: 'GET', url: '/api/users' },
|
|
136
|
+
{ method: 'POST', url: '/api/users' },
|
|
137
|
+
{ method: 'PUT', url: '/api/users/:id' },
|
|
138
|
+
]);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Track with whitelist:**
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
const middleware = usageFlow.createMiddleware(
|
|
145
|
+
[
|
|
146
|
+
{ method: '*', url: '*' }, // Track all routes
|
|
147
|
+
],
|
|
148
|
+
[
|
|
149
|
+
{ method: 'GET', url: '/api/health' }, // Exclude health check
|
|
150
|
+
{ method: 'GET', url: '/api/metrics' }, // Exclude metrics
|
|
151
|
+
]
|
|
75
152
|
);
|
|
76
153
|
```
|
|
77
154
|
|
|
78
|
-
##
|
|
155
|
+
## Features
|
|
156
|
+
|
|
157
|
+
- **Automatic Route Detection**: Automatically detects route patterns from Express router
|
|
158
|
+
- **Request Metadata Collection**: Collects comprehensive request metadata including headers, query params, path params, and body
|
|
159
|
+
- **Response Tracking**: Tracks response status codes and duration
|
|
160
|
+
- **WebSocket Communication**: Uses WebSocket for real-time communication with UsageFlow API
|
|
161
|
+
- **Connection Pooling**: Maintains a pool of WebSocket connections for better performance
|
|
162
|
+
- **Header Sanitization**: Automatically sanitizes sensitive headers (authorization, API keys)
|
|
163
|
+
- **Error Handling**: Gracefully handles errors and provides meaningful error messages
|
|
164
|
+
|
|
165
|
+
## Request Metadata
|
|
166
|
+
|
|
167
|
+
The middleware automatically collects the following metadata for each request:
|
|
168
|
+
|
|
169
|
+
- HTTP method
|
|
170
|
+
- Route pattern
|
|
171
|
+
- Raw URL
|
|
172
|
+
- Client IP (with X-Forwarded-For support)
|
|
173
|
+
- User agent
|
|
174
|
+
- Timestamp
|
|
175
|
+
- Headers (sanitized)
|
|
176
|
+
- Query parameters
|
|
177
|
+
- Path parameters
|
|
178
|
+
- Request body
|
|
179
|
+
- Response status code
|
|
180
|
+
- Request duration
|
|
181
|
+
|
|
182
|
+
## Error Handling
|
|
183
|
+
|
|
184
|
+
If an allocation request fails, the middleware will:
|
|
185
|
+
|
|
186
|
+
1. Return a 400 status code
|
|
187
|
+
2. Include an error message in the response
|
|
188
|
+
3. Set `blocked: true` in the response body
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
// Error response format
|
|
192
|
+
{
|
|
193
|
+
message: "Error message",
|
|
194
|
+
blocked: true
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Advanced Usage
|
|
199
|
+
|
|
200
|
+
### Custom Route Pattern Extraction
|
|
201
|
+
|
|
202
|
+
The middleware automatically extracts route patterns from Express. It supports:
|
|
203
|
+
|
|
204
|
+
- Direct route paths (`req.route.path`)
|
|
205
|
+
- Router stack traversal
|
|
206
|
+
- Nested routers
|
|
207
|
+
- Parameterized routes (`/users/:id`)
|
|
208
|
+
|
|
209
|
+
### Ledger ID Generation
|
|
210
|
+
|
|
211
|
+
The middleware automatically generates ledger IDs based on:
|
|
212
|
+
|
|
213
|
+
1. HTTP method and route pattern
|
|
214
|
+
2. Configured identity fields from UsageFlow policies
|
|
215
|
+
3. Identity field locations (path params, query params, body, bearer token, headers)
|
|
79
216
|
|
|
80
|
-
|
|
217
|
+
## Requirements
|
|
81
218
|
|
|
82
|
-
-
|
|
83
|
-
-
|
|
84
|
-
-
|
|
219
|
+
- Node.js >= 18.0.0
|
|
220
|
+
- Express >= 4.17.0
|
|
221
|
+
- TypeScript >= 5.0.0 (for TypeScript projects)
|
|
85
222
|
|
|
86
|
-
|
|
223
|
+
## Dependencies
|
|
224
|
+
|
|
225
|
+
- `@usageflow/core`: Core UsageFlow functionality
|
|
226
|
+
- `express`: Express.js framework
|
|
227
|
+
|
|
228
|
+
## Development
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
# Install dependencies
|
|
232
|
+
npm install
|
|
233
|
+
|
|
234
|
+
# Build the package
|
|
235
|
+
npm run build
|
|
236
|
+
|
|
237
|
+
# Run tests
|
|
238
|
+
npm test
|
|
239
|
+
```
|
|
87
240
|
|
|
88
241
|
## License
|
|
89
242
|
|
|
90
243
|
MIT
|
|
244
|
+
|
|
245
|
+
## Support
|
|
246
|
+
|
|
247
|
+
For issues, questions, or contributions, please visit our [GitHub repository](https://github.com/usageflow/js-mongorepo).
|
package/dist/plugin.js
CHANGED
|
@@ -44,7 +44,7 @@ class ExpressUsageFlowAPI extends core_1.UsageFlowAPI {
|
|
|
44
44
|
};
|
|
45
45
|
return metadata;
|
|
46
46
|
}
|
|
47
|
-
async executeRequestWithMetadata(ledgerId, metadata, request, response) {
|
|
47
|
+
async executeRequestWithMetadata(ledgerId, metadata, request, response, hasLimit) {
|
|
48
48
|
if (!this.apiKey) {
|
|
49
49
|
throw new Error("API key not initialized");
|
|
50
50
|
}
|
|
@@ -54,7 +54,7 @@ class ExpressUsageFlowAPI extends core_1.UsageFlowAPI {
|
|
|
54
54
|
metadata,
|
|
55
55
|
duration: 1000
|
|
56
56
|
};
|
|
57
|
-
await this.allocationRequest(request, payload, metadata);
|
|
57
|
+
await this.allocationRequest(request, payload, metadata, hasLimit);
|
|
58
58
|
}
|
|
59
59
|
createMiddleware(routes, whitelistRoutes = []) {
|
|
60
60
|
const routesMap = this.createRoutesMap(routes);
|
|
@@ -75,9 +75,12 @@ class ExpressUsageFlowAPI extends core_1.UsageFlowAPI {
|
|
|
75
75
|
}
|
|
76
76
|
const metadata = await this.collectRequestMetadata(request);
|
|
77
77
|
metadata.url = url;
|
|
78
|
-
|
|
78
|
+
const { ledgerId, hasLimit } = this.guessLedgerId(request);
|
|
79
79
|
try {
|
|
80
|
-
|
|
80
|
+
if (this.blockedEndpoints.includes(ledgerId)) {
|
|
81
|
+
throw new core_1.UsageFlowError(`Endpoint is been blocked by the administrator`);
|
|
82
|
+
}
|
|
83
|
+
await this.executeRequestWithMetadata(ledgerId, metadata, request, response, hasLimit);
|
|
81
84
|
// Capture response data
|
|
82
85
|
const originalEnd = response.end;
|
|
83
86
|
response.end = function (chunk, encoding, cb) {
|
package/dist/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":";;;AACA,
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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"}
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
preset: 'ts-jest',
|
|
3
|
+
testEnvironment: 'node',
|
|
4
|
+
roots: ['<rootDir>/test'],
|
|
5
|
+
testMatch: ['**/*.test.ts'],
|
|
6
|
+
transform: {
|
|
7
|
+
'^.+\\.ts$': 'ts-jest',
|
|
8
|
+
},
|
|
9
|
+
moduleFileExtensions: ['ts', 'js', 'json'],
|
|
10
|
+
collectCoverageFrom: ['src/**/*.ts'],
|
|
11
|
+
coverageDirectory: 'coverage',
|
|
12
|
+
testTimeout: 10000,
|
|
13
|
+
};
|
|
14
|
+
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usageflow/express",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "UsageFlow plugin for Express applications",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc",
|
|
9
|
-
"test": "
|
|
9
|
+
"test": "tsx --test test/plugin.test.ts",
|
|
10
10
|
"prepare": "npm run build",
|
|
11
11
|
"prepublishOnly": "npm run build"
|
|
12
12
|
},
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"access": "public"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@usageflow/core": "^0.
|
|
17
|
+
"@usageflow/core": "^0.3.0",
|
|
18
|
+
"@usageflow/logger": "^0.1.1",
|
|
18
19
|
"express": "^4.18.0",
|
|
19
20
|
"ws": "^8.16.0"
|
|
20
21
|
},
|
|
@@ -29,9 +30,7 @@
|
|
|
29
30
|
"@types/node": "^20.0.0",
|
|
30
31
|
"@types/ws": "^8.5.10",
|
|
31
32
|
"typescript": "^5.0.0",
|
|
32
|
-
"
|
|
33
|
-
"@types/jest": "^29.0.0",
|
|
34
|
-
"ts-jest": "^29.0.0"
|
|
33
|
+
"tsx": "^4.7.0"
|
|
35
34
|
},
|
|
36
35
|
"peerDependencies": {
|
|
37
36
|
"express": ">=4.17.0"
|
package/src/plugin.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Request, Response, NextFunction } from "express";
|
|
2
|
-
import { UsageFlowAPI, Route, RequestMetadata, RequestForAllocation, UsageFlowRequest, UsageFlowAPIConfig } from "@usageflow/core";
|
|
2
|
+
import { UsageFlowAPI, Route, RequestMetadata, RequestForAllocation, UsageFlowRequest, UsageFlowAPIConfig, UsageFlowError } from "@usageflow/core";
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
|
|
@@ -61,6 +61,7 @@ export class ExpressUsageFlowAPI extends UsageFlowAPI {
|
|
|
61
61
|
metadata: RequestMetadata,
|
|
62
62
|
request: Request,
|
|
63
63
|
response: Response,
|
|
64
|
+
hasLimit: boolean,
|
|
64
65
|
): Promise<void> {
|
|
65
66
|
if (!this.apiKey) {
|
|
66
67
|
throw new Error("API key not initialized");
|
|
@@ -73,7 +74,7 @@ export class ExpressUsageFlowAPI extends UsageFlowAPI {
|
|
|
73
74
|
duration: 1000
|
|
74
75
|
};
|
|
75
76
|
|
|
76
|
-
await this.allocationRequest(request as unknown as UsageFlowRequest, payload, metadata);
|
|
77
|
+
await this.allocationRequest(request as unknown as UsageFlowRequest, payload, metadata, hasLimit);
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
public createMiddleware(routes: Route[], whitelistRoutes: Route[] = []) {
|
|
@@ -101,14 +102,20 @@ export class ExpressUsageFlowAPI extends UsageFlowAPI {
|
|
|
101
102
|
|
|
102
103
|
const metadata = await this.collectRequestMetadata(request);
|
|
103
104
|
metadata.url = url;
|
|
104
|
-
|
|
105
|
+
const { ledgerId, hasLimit } = this.guessLedgerId(request);
|
|
106
|
+
|
|
105
107
|
|
|
106
108
|
try {
|
|
109
|
+
if (this.blockedEndpoints.includes(ledgerId)) {
|
|
110
|
+
throw new UsageFlowError(`Endpoint is been blocked by the administrator`);
|
|
111
|
+
}
|
|
112
|
+
|
|
107
113
|
await this.executeRequestWithMetadata(
|
|
108
114
|
ledgerId,
|
|
109
115
|
metadata,
|
|
110
116
|
request,
|
|
111
117
|
response,
|
|
118
|
+
hasLimit,
|
|
112
119
|
);
|
|
113
120
|
|
|
114
121
|
// Capture response data
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { test, describe, beforeEach, afterEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { ExpressUsageFlowAPI } from '../src/plugin';
|
|
4
|
+
import { Route } from '@usageflow/core';
|
|
5
|
+
|
|
6
|
+
describe('ExpressUsageFlowAPI', () => {
|
|
7
|
+
let api: ExpressUsageFlowAPI;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
api = new ExpressUsageFlowAPI({
|
|
11
|
+
apiKey: 'test-api-key',
|
|
12
|
+
poolSize: 1
|
|
13
|
+
});
|
|
14
|
+
// Mock socket manager
|
|
15
|
+
api.socketManager = {
|
|
16
|
+
connected: false,
|
|
17
|
+
async connect() { this.connected = true; },
|
|
18
|
+
isConnected() { return this.connected; },
|
|
19
|
+
async sendAsync<T>(payload: any): Promise<any> {
|
|
20
|
+
return { type: 'success', payload: { allocationId: 'test-id' } };
|
|
21
|
+
},
|
|
22
|
+
send() { },
|
|
23
|
+
close() { },
|
|
24
|
+
destroy() { }
|
|
25
|
+
} as any;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
api.destroy();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('should create middleware', () => {
|
|
33
|
+
const routes: Route[] = [
|
|
34
|
+
{ method: 'GET', url: '/users' }
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const middleware = api.createMiddleware(routes);
|
|
38
|
+
assert.strictEqual(typeof middleware, 'function');
|
|
39
|
+
assert.strictEqual(middleware.length, 3); // Express middleware signature
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('should create middleware with whitelist', () => {
|
|
43
|
+
const routes: Route[] = [
|
|
44
|
+
{ method: '*', url: '*' }
|
|
45
|
+
];
|
|
46
|
+
const whitelistRoutes: Route[] = [
|
|
47
|
+
{ method: 'GET', url: '/health' }
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const middleware = api.createMiddleware(routes, whitelistRoutes);
|
|
51
|
+
assert.strictEqual(typeof middleware, 'function');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test.skip('should skip whitelisted routes', async () => {
|
|
55
|
+
const routes: Route[] = [
|
|
56
|
+
{ method: '*', url: '*' }
|
|
57
|
+
];
|
|
58
|
+
const whitelistRoutes: Route[] = [
|
|
59
|
+
{ method: 'GET', url: '/health' }
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const middleware = api.createMiddleware(routes, whitelistRoutes);
|
|
63
|
+
|
|
64
|
+
const req: any = {
|
|
65
|
+
method: 'GET',
|
|
66
|
+
url: '/health',
|
|
67
|
+
path: '/health',
|
|
68
|
+
route: { path: '/health' },
|
|
69
|
+
headers: {},
|
|
70
|
+
query: {},
|
|
71
|
+
params: {},
|
|
72
|
+
body: {},
|
|
73
|
+
app: { _router: { stack: [] } },
|
|
74
|
+
usageflow: undefined
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const res: any = {
|
|
78
|
+
statusCode: 200,
|
|
79
|
+
end: (chunk?: any, encoding?: any, cb?: any) => {
|
|
80
|
+
if (typeof chunk === 'function') {
|
|
81
|
+
chunk();
|
|
82
|
+
} else if (typeof encoding === 'function') {
|
|
83
|
+
encoding();
|
|
84
|
+
} else if (cb) {
|
|
85
|
+
cb();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
let nextCalled = false;
|
|
91
|
+
const next = () => { nextCalled = true; };
|
|
92
|
+
|
|
93
|
+
await middleware(req, res, next);
|
|
94
|
+
|
|
95
|
+
assert.strictEqual(nextCalled, true);
|
|
96
|
+
assert.strictEqual(req.usageflow, undefined);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('should monitor matching routes', async () => {
|
|
100
|
+
const routes: Route[] = [
|
|
101
|
+
{ method: 'GET', url: '/users' }
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const middleware = api.createMiddleware(routes);
|
|
105
|
+
|
|
106
|
+
const req: any = {
|
|
107
|
+
method: 'GET',
|
|
108
|
+
url: '/users',
|
|
109
|
+
path: '/users',
|
|
110
|
+
route: { path: '/users' },
|
|
111
|
+
headers: {},
|
|
112
|
+
query: {},
|
|
113
|
+
params: {},
|
|
114
|
+
body: {},
|
|
115
|
+
ip: '127.0.0.1',
|
|
116
|
+
originalUrl: '/users',
|
|
117
|
+
app: { _router: { stack: [] } },
|
|
118
|
+
usageflow: undefined
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const res: any = {
|
|
122
|
+
statusCode: 200,
|
|
123
|
+
end: (chunk?: any, encoding?: any, cb?: any) => {
|
|
124
|
+
if (typeof chunk === 'function') {
|
|
125
|
+
chunk();
|
|
126
|
+
} else if (typeof encoding === 'function') {
|
|
127
|
+
encoding();
|
|
128
|
+
} else if (cb) {
|
|
129
|
+
cb();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
let nextCalled = false;
|
|
135
|
+
const next = () => { nextCalled = true; };
|
|
136
|
+
|
|
137
|
+
await middleware(req, res, next);
|
|
138
|
+
|
|
139
|
+
assert.strictEqual(nextCalled, true);
|
|
140
|
+
assert.ok(req.usageflow);
|
|
141
|
+
assert.ok(req.usageflow.startTime);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test.skip('should handle errors', async () => {
|
|
145
|
+
// Make socket manager throw error
|
|
146
|
+
api.socketManager = {
|
|
147
|
+
connected: false,
|
|
148
|
+
async connect() { this.connected = true; },
|
|
149
|
+
isConnected() { return this.connected; },
|
|
150
|
+
async sendAsync<T>(payload: any): Promise<any> {
|
|
151
|
+
throw new Error('Connection failed');
|
|
152
|
+
},
|
|
153
|
+
send() { },
|
|
154
|
+
close() { },
|
|
155
|
+
destroy() { }
|
|
156
|
+
} as any;
|
|
157
|
+
|
|
158
|
+
const routes: Route[] = [
|
|
159
|
+
{ method: 'GET', url: '/users' }
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
const middleware = api.createMiddleware(routes);
|
|
163
|
+
|
|
164
|
+
const req: any = {
|
|
165
|
+
method: 'GET',
|
|
166
|
+
url: '/users',
|
|
167
|
+
path: '/users',
|
|
168
|
+
route: { path: '/users' },
|
|
169
|
+
headers: {},
|
|
170
|
+
query: {},
|
|
171
|
+
params: {},
|
|
172
|
+
body: {},
|
|
173
|
+
ip: '127.0.0.1',
|
|
174
|
+
originalUrl: '/users',
|
|
175
|
+
app: { _router: { stack: [] } },
|
|
176
|
+
usageflow: undefined
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
let statusCode = 200;
|
|
180
|
+
let responseBody: any = null;
|
|
181
|
+
const res: any = {
|
|
182
|
+
statusCode: 200,
|
|
183
|
+
status: (code: number) => {
|
|
184
|
+
statusCode = code;
|
|
185
|
+
return res;
|
|
186
|
+
},
|
|
187
|
+
json: (body: any) => {
|
|
188
|
+
responseBody = body;
|
|
189
|
+
},
|
|
190
|
+
end: () => { }
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const next = () => { };
|
|
194
|
+
|
|
195
|
+
await middleware(req, res, next);
|
|
196
|
+
|
|
197
|
+
assert.strictEqual(statusCode, 400);
|
|
198
|
+
assert.strictEqual(responseBody.blocked, true);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
package/tsconfig.json
CHANGED