navis.js 3.1.0 → 5.0.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 +30 -2
- package/examples/v4-features-demo.js +171 -0
- package/examples/v5-features-demo.js +167 -0
- package/package.json +1 -1
- package/src/auth/authenticator.js +223 -0
- package/src/cache/cache.js +157 -0
- package/src/cache/redis-cache.js +174 -0
- package/src/core/advanced-router.js +186 -0
- package/src/core/app.js +264 -176
- package/src/core/graceful-shutdown.js +77 -0
- package/src/errors/error-handler.js +157 -0
- package/src/health/health-checker.js +120 -0
- package/src/index.js +69 -0
- package/src/middleware/cache-middleware.js +105 -0
- package/src/middleware/compression.js +97 -0
- package/src/middleware/cors.js +86 -0
- package/src/middleware/rate-limiter.js +159 -0
- package/src/middleware/security.js +107 -0
- package/src/validation/validator.js +301 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-Memory Cache
|
|
3
|
+
* v5: Simple in-memory caching with TTL support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class Cache {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.store = new Map();
|
|
9
|
+
this.maxSize = options.maxSize || 1000;
|
|
10
|
+
this.defaultTTL = options.defaultTTL || 3600000; // 1 hour in ms
|
|
11
|
+
this.cleanupInterval = options.cleanupInterval || 60000; // 1 minute
|
|
12
|
+
|
|
13
|
+
// Start cleanup interval
|
|
14
|
+
this.intervalId = setInterval(() => {
|
|
15
|
+
this._cleanup();
|
|
16
|
+
}, this.cleanupInterval);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Set a value in cache
|
|
21
|
+
* @param {string} key - Cache key
|
|
22
|
+
* @param {*} value - Value to cache
|
|
23
|
+
* @param {number} ttl - Time to live in milliseconds
|
|
24
|
+
*/
|
|
25
|
+
set(key, value, ttl = null) {
|
|
26
|
+
const expiration = Date.now() + (ttl || this.defaultTTL);
|
|
27
|
+
|
|
28
|
+
// Remove oldest entries if at max size
|
|
29
|
+
if (this.store.size >= this.maxSize && !this.store.has(key)) {
|
|
30
|
+
this._evictOldest();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.store.set(key, {
|
|
34
|
+
value,
|
|
35
|
+
expiration,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get a value from cache
|
|
41
|
+
* @param {string} key - Cache key
|
|
42
|
+
* @returns {*} - Cached value or null
|
|
43
|
+
*/
|
|
44
|
+
get(key) {
|
|
45
|
+
const entry = this.store.get(key);
|
|
46
|
+
|
|
47
|
+
if (!entry) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check if expired
|
|
52
|
+
if (Date.now() > entry.expiration) {
|
|
53
|
+
this.store.delete(key);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return entry.value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if key exists in cache
|
|
62
|
+
* @param {string} key - Cache key
|
|
63
|
+
* @returns {boolean} - True if key exists and not expired
|
|
64
|
+
*/
|
|
65
|
+
has(key) {
|
|
66
|
+
const entry = this.store.get(key);
|
|
67
|
+
|
|
68
|
+
if (!entry) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (Date.now() > entry.expiration) {
|
|
73
|
+
this.store.delete(key);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Delete a key from cache
|
|
82
|
+
* @param {string} key - Cache key
|
|
83
|
+
* @returns {boolean} - True if key was deleted
|
|
84
|
+
*/
|
|
85
|
+
delete(key) {
|
|
86
|
+
return this.store.delete(key);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Clear all cache entries
|
|
91
|
+
*/
|
|
92
|
+
clear() {
|
|
93
|
+
this.store.clear();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get cache size
|
|
98
|
+
* @returns {number} - Number of entries
|
|
99
|
+
*/
|
|
100
|
+
size() {
|
|
101
|
+
return this.store.size;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get all keys
|
|
106
|
+
* @returns {Array} - Array of cache keys
|
|
107
|
+
*/
|
|
108
|
+
keys() {
|
|
109
|
+
return Array.from(this.store.keys());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Cleanup expired entries
|
|
114
|
+
* @private
|
|
115
|
+
*/
|
|
116
|
+
_cleanup() {
|
|
117
|
+
const now = Date.now();
|
|
118
|
+
for (const [key, entry] of this.store.entries()) {
|
|
119
|
+
if (now > entry.expiration) {
|
|
120
|
+
this.store.delete(key);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Evict oldest entry (LRU-like)
|
|
127
|
+
* @private
|
|
128
|
+
*/
|
|
129
|
+
_evictOldest() {
|
|
130
|
+
let oldestKey = null;
|
|
131
|
+
let oldestExpiration = Infinity;
|
|
132
|
+
|
|
133
|
+
for (const [key, entry] of this.store.entries()) {
|
|
134
|
+
if (entry.expiration < oldestExpiration) {
|
|
135
|
+
oldestExpiration = entry.expiration;
|
|
136
|
+
oldestKey = key;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (oldestKey) {
|
|
141
|
+
this.store.delete(oldestKey);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Destroy cache (cleanup)
|
|
147
|
+
*/
|
|
148
|
+
destroy() {
|
|
149
|
+
if (this.intervalId) {
|
|
150
|
+
clearInterval(this.intervalId);
|
|
151
|
+
}
|
|
152
|
+
this.clear();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = Cache;
|
|
157
|
+
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis Cache Adapter
|
|
3
|
+
* v5: Redis-based caching (requires redis package)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class RedisCache {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.client = null;
|
|
9
|
+
this.defaultTTL = options.defaultTTL || 3600; // 1 hour in seconds
|
|
10
|
+
this.prefix = options.prefix || 'navis:';
|
|
11
|
+
this.connected = false;
|
|
12
|
+
|
|
13
|
+
// Try to require redis (optional dependency)
|
|
14
|
+
try {
|
|
15
|
+
const redis = require('redis');
|
|
16
|
+
this.redis = redis;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
throw new Error('Redis package not installed. Install with: npm install redis');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Connect to Redis
|
|
24
|
+
* @param {Object} options - Redis connection options
|
|
25
|
+
*/
|
|
26
|
+
async connect(options = {}) {
|
|
27
|
+
if (this.connected && this.client) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
url = process.env.REDIS_URL || 'redis://localhost:6379',
|
|
33
|
+
socket = {},
|
|
34
|
+
...otherOptions
|
|
35
|
+
} = options;
|
|
36
|
+
|
|
37
|
+
this.client = this.redis.createClient({
|
|
38
|
+
url,
|
|
39
|
+
socket: {
|
|
40
|
+
reconnectStrategy: (retries) => {
|
|
41
|
+
if (retries > 10) {
|
|
42
|
+
return new Error('Too many reconnection attempts');
|
|
43
|
+
}
|
|
44
|
+
return Math.min(retries * 100, 3000);
|
|
45
|
+
},
|
|
46
|
+
...socket,
|
|
47
|
+
},
|
|
48
|
+
...otherOptions,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
this.client.on('error', (err) => {
|
|
52
|
+
console.error('Redis Client Error:', err);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await this.client.connect();
|
|
56
|
+
this.connected = true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Disconnect from Redis
|
|
61
|
+
*/
|
|
62
|
+
async disconnect() {
|
|
63
|
+
if (this.client && this.connected) {
|
|
64
|
+
await this.client.quit();
|
|
65
|
+
this.connected = false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Set a value in cache
|
|
71
|
+
* @param {string} key - Cache key
|
|
72
|
+
* @param {*} value - Value to cache
|
|
73
|
+
* @param {number} ttl - Time to live in seconds
|
|
74
|
+
*/
|
|
75
|
+
async set(key, value, ttl = null) {
|
|
76
|
+
if (!this.connected) {
|
|
77
|
+
throw new Error('Redis not connected. Call connect() first.');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const fullKey = this.prefix + key;
|
|
81
|
+
const serialized = JSON.stringify(value);
|
|
82
|
+
const expiration = ttl || this.defaultTTL;
|
|
83
|
+
|
|
84
|
+
if (expiration > 0) {
|
|
85
|
+
await this.client.setEx(fullKey, expiration, serialized);
|
|
86
|
+
} else {
|
|
87
|
+
await this.client.set(fullKey, serialized);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get a value from cache
|
|
93
|
+
* @param {string} key - Cache key
|
|
94
|
+
* @returns {*} - Cached value or null
|
|
95
|
+
*/
|
|
96
|
+
async get(key) {
|
|
97
|
+
if (!this.connected) {
|
|
98
|
+
throw new Error('Redis not connected. Call connect() first.');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const fullKey = this.prefix + key;
|
|
102
|
+
const serialized = await this.client.get(fullKey);
|
|
103
|
+
|
|
104
|
+
if (!serialized) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(serialized);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Check if key exists in cache
|
|
117
|
+
* @param {string} key - Cache key
|
|
118
|
+
* @returns {boolean} - True if key exists
|
|
119
|
+
*/
|
|
120
|
+
async has(key) {
|
|
121
|
+
if (!this.connected) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const fullKey = this.prefix + key;
|
|
126
|
+
const exists = await this.client.exists(fullKey);
|
|
127
|
+
return exists === 1;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Delete a key from cache
|
|
132
|
+
* @param {string} key - Cache key
|
|
133
|
+
* @returns {boolean} - True if key was deleted
|
|
134
|
+
*/
|
|
135
|
+
async delete(key) {
|
|
136
|
+
if (!this.connected) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const fullKey = this.prefix + key;
|
|
141
|
+
const result = await this.client.del(fullKey);
|
|
142
|
+
return result > 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Clear all cache entries with prefix
|
|
147
|
+
*/
|
|
148
|
+
async clear() {
|
|
149
|
+
if (!this.connected) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const keys = await this.client.keys(this.prefix + '*');
|
|
154
|
+
if (keys.length > 0) {
|
|
155
|
+
await this.client.del(keys);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get cache size (approximate)
|
|
161
|
+
* @returns {number} - Number of keys with prefix
|
|
162
|
+
*/
|
|
163
|
+
async size() {
|
|
164
|
+
if (!this.connected) {
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const keys = await this.client.keys(this.prefix + '*');
|
|
169
|
+
return keys.length;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = RedisCache;
|
|
174
|
+
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced Router with Parameters and Path Matching
|
|
3
|
+
* v4: Support for route parameters, wildcards, and path matching
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class AdvancedRouter {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.routes = {
|
|
9
|
+
GET: [],
|
|
10
|
+
POST: [],
|
|
11
|
+
PUT: [],
|
|
12
|
+
DELETE: [],
|
|
13
|
+
PATCH: [],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Register a route handler
|
|
19
|
+
* @param {string} method - HTTP method
|
|
20
|
+
* @param {string} path - Route path (supports :param and * wildcards)
|
|
21
|
+
* @param {Function} handler - Route handler function
|
|
22
|
+
*/
|
|
23
|
+
register(method, path, handler) {
|
|
24
|
+
const normalizedMethod = method.toUpperCase();
|
|
25
|
+
if (!this.routes[normalizedMethod]) {
|
|
26
|
+
throw new Error(`Unsupported HTTP method: ${method}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Parse route pattern
|
|
30
|
+
const pattern = this._parsePattern(path);
|
|
31
|
+
|
|
32
|
+
this.routes[normalizedMethod].push({
|
|
33
|
+
pattern: path,
|
|
34
|
+
regex: pattern.regex,
|
|
35
|
+
params: pattern.params,
|
|
36
|
+
handler,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Sort routes by specificity (more specific first)
|
|
40
|
+
this.routes[normalizedMethod].sort((a, b) => {
|
|
41
|
+
const aSpecificity = this._calculateSpecificity(a.pattern);
|
|
42
|
+
const bSpecificity = this._calculateSpecificity(b.pattern);
|
|
43
|
+
return bSpecificity - aSpecificity;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Find route handler for a method and path
|
|
49
|
+
* @param {string} method - HTTP method
|
|
50
|
+
* @param {string} path - Request path
|
|
51
|
+
* @returns {Object|null} - { handler, params } or null if not found
|
|
52
|
+
*/
|
|
53
|
+
find(method, path) {
|
|
54
|
+
const normalizedMethod = method.toUpperCase();
|
|
55
|
+
const methodRoutes = this.routes[normalizedMethod] || [];
|
|
56
|
+
|
|
57
|
+
for (const route of methodRoutes) {
|
|
58
|
+
const match = path.match(route.regex);
|
|
59
|
+
if (match) {
|
|
60
|
+
// Extract parameters
|
|
61
|
+
const params = {};
|
|
62
|
+
route.params.forEach((param, index) => {
|
|
63
|
+
params[param] = match[index + 1];
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
handler: route.handler,
|
|
68
|
+
params,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Parse route pattern into regex and parameter names
|
|
78
|
+
* @private
|
|
79
|
+
*/
|
|
80
|
+
_parsePattern(pattern) {
|
|
81
|
+
const params = [];
|
|
82
|
+
const parts = pattern.split('/').filter(p => p !== ''); // Remove empty parts
|
|
83
|
+
const regexParts = [];
|
|
84
|
+
|
|
85
|
+
for (const part of parts) {
|
|
86
|
+
if (part.startsWith(':')) {
|
|
87
|
+
// Parameter
|
|
88
|
+
const paramName = part.substring(1);
|
|
89
|
+
params.push(paramName);
|
|
90
|
+
regexParts.push('([^/]+)');
|
|
91
|
+
} else if (part === '*') {
|
|
92
|
+
// Wildcard
|
|
93
|
+
regexParts.push('.*');
|
|
94
|
+
} else {
|
|
95
|
+
// Literal - escape regex special chars
|
|
96
|
+
regexParts.push(part.replace(/[.+?^${}()|[\]\\]/g, '\\$&'));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Build regex pattern
|
|
101
|
+
const regexPattern = '^/' + regexParts.join('/') + '/?$';
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
regex: new RegExp(regexPattern),
|
|
105
|
+
params,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Calculate route specificity (higher = more specific)
|
|
111
|
+
* @private
|
|
112
|
+
*/
|
|
113
|
+
_calculateSpecificity(pattern) {
|
|
114
|
+
let specificity = 0;
|
|
115
|
+
|
|
116
|
+
// Count static segments
|
|
117
|
+
const staticSegments = pattern.split('/').filter(seg =>
|
|
118
|
+
seg && !seg.startsWith(':') && seg !== '*'
|
|
119
|
+
);
|
|
120
|
+
specificity += staticSegments.length * 10;
|
|
121
|
+
|
|
122
|
+
// Count parameters (less specific)
|
|
123
|
+
const params = (pattern.match(/:[^/]+/g) || []).length;
|
|
124
|
+
specificity += params * 5;
|
|
125
|
+
|
|
126
|
+
// Wildcards are least specific
|
|
127
|
+
if (pattern.includes('*')) {
|
|
128
|
+
specificity -= 10;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return specificity;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get all registered routes
|
|
136
|
+
* @returns {Object} - All routes grouped by method
|
|
137
|
+
*/
|
|
138
|
+
getAllRoutes() {
|
|
139
|
+
const allRoutes = {};
|
|
140
|
+
for (const method in this.routes) {
|
|
141
|
+
allRoutes[method] = this.routes[method].map(route => ({
|
|
142
|
+
pattern: route.pattern,
|
|
143
|
+
params: route.params,
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
return allRoutes;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Register GET route
|
|
151
|
+
*/
|
|
152
|
+
get(path, handler) {
|
|
153
|
+
this.register('GET', path, handler);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Register POST route
|
|
158
|
+
*/
|
|
159
|
+
post(path, handler) {
|
|
160
|
+
this.register('POST', path, handler);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Register PUT route
|
|
165
|
+
*/
|
|
166
|
+
put(path, handler) {
|
|
167
|
+
this.register('PUT', path, handler);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Register DELETE route
|
|
172
|
+
*/
|
|
173
|
+
delete(path, handler) {
|
|
174
|
+
this.register('DELETE', path, handler);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Register PATCH route
|
|
179
|
+
*/
|
|
180
|
+
patch(path, handler) {
|
|
181
|
+
this.register('PATCH', path, handler);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = AdvancedRouter;
|
|
186
|
+
|