@naman_deep_singh/server-utils 1.0.8 → 1.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.
Files changed (51) hide show
  1. package/README.md +401 -11
  2. package/dist/{index.d.ts → cjs/index.d.ts} +1 -1
  3. package/dist/{index.js → cjs/index.js} +3 -1
  4. package/dist/{middleware.d.ts → cjs/middleware.d.ts} +2 -0
  5. package/dist/{middleware.js → cjs/middleware.js} +137 -10
  6. package/dist/{server.d.ts → cjs/server.d.ts} +1 -0
  7. package/dist/{server.js → cjs/server.js} +119 -4
  8. package/dist/{types.d.ts → cjs/types.d.ts} +17 -0
  9. package/dist/{utils.js → cjs/utils.js} +14 -8
  10. package/dist/esm/health.d.ts +5 -0
  11. package/dist/esm/health.js +40 -0
  12. package/dist/esm/index.d.ts +46 -0
  13. package/dist/esm/index.js +58 -0
  14. package/dist/esm/middleware.d.ts +39 -0
  15. package/dist/esm/middleware.js +327 -0
  16. package/dist/esm/periodic-health.d.ts +11 -0
  17. package/dist/esm/periodic-health.js +64 -0
  18. package/dist/esm/server.d.ts +70 -0
  19. package/dist/esm/server.js +386 -0
  20. package/dist/esm/shutdown.d.ts +5 -0
  21. package/dist/esm/shutdown.js +52 -0
  22. package/dist/esm/types.d.ts +87 -0
  23. package/dist/esm/types.js +1 -0
  24. package/dist/esm/utils.d.ts +3 -0
  25. package/dist/esm/utils.js +38 -0
  26. package/dist/types/health.d.ts +5 -0
  27. package/dist/types/index.d.ts +46 -0
  28. package/dist/types/middleware.d.ts +39 -0
  29. package/dist/types/periodic-health.d.ts +11 -0
  30. package/dist/types/server.d.ts +70 -0
  31. package/dist/types/shutdown.d.ts +5 -0
  32. package/dist/types/types.d.ts +87 -0
  33. package/dist/types/utils.d.ts +3 -0
  34. package/package.json +26 -10
  35. package/src/health.ts +0 -47
  36. package/src/index.ts +0 -126
  37. package/src/middleware.ts +0 -275
  38. package/src/periodic-health.ts +0 -83
  39. package/src/server.ts +0 -412
  40. package/src/shutdown.ts +0 -69
  41. package/src/types.ts +0 -80
  42. package/src/utils.ts +0 -34
  43. package/tsconfig.json +0 -21
  44. /package/dist/{health.d.ts → cjs/health.d.ts} +0 -0
  45. /package/dist/{health.js → cjs/health.js} +0 -0
  46. /package/dist/{periodic-health.d.ts → cjs/periodic-health.d.ts} +0 -0
  47. /package/dist/{periodic-health.js → cjs/periodic-health.js} +0 -0
  48. /package/dist/{shutdown.d.ts → cjs/shutdown.d.ts} +0 -0
  49. /package/dist/{shutdown.js → cjs/shutdown.js} +0 -0
  50. /package/dist/{types.js → cjs/types.js} +0 -0
  51. /package/dist/{utils.d.ts → cjs/utils.d.ts} +0 -0
@@ -0,0 +1,327 @@
1
+ // Logging middleware
2
+ export function createLoggingMiddleware(format = 'simple') {
3
+ return (req, res, next) => {
4
+ const start = Date.now();
5
+ res.on('finish', () => {
6
+ const duration = Date.now() - start;
7
+ if (format === 'detailed') {
8
+ console.log(`${req.method} ${req.url} - ${res.statusCode} - ${duration}ms - ${req.ip}`);
9
+ }
10
+ else {
11
+ console.log(`${req.method} ${req.url} - ${res.statusCode}`);
12
+ }
13
+ });
14
+ next();
15
+ };
16
+ }
17
+ // Error handling middleware
18
+ export function createErrorHandler() {
19
+ return (err, req, res, next) => {
20
+ console.error('Error:', err);
21
+ if (res.headersSent) {
22
+ return next(err);
23
+ }
24
+ // Type guard for error objects
25
+ const errorObj = err;
26
+ const status = errorObj.status || errorObj.statusCode || 500;
27
+ const message = process.env.NODE_ENV === 'production'
28
+ ? 'Internal Server Error'
29
+ : errorObj.message || 'Unknown error';
30
+ res.status(status).json({
31
+ success: false,
32
+ message,
33
+ data: undefined,
34
+ error: {
35
+ message,
36
+ ...(process.env.NODE_ENV !== 'production' && { details: { stack: errorObj.stack } })
37
+ },
38
+ meta: null
39
+ });
40
+ };
41
+ }
42
+ // Request ID middleware
43
+ export function createRequestIdMiddleware() {
44
+ return (req, res, next) => {
45
+ const requestId = Math.random().toString(36).substring(2, 15);
46
+ req.requestId = requestId;
47
+ res.setHeader('X-Request-ID', requestId);
48
+ next();
49
+ };
50
+ }
51
+ export function createValidationMiddleware(rules) {
52
+ return (req, res, next) => {
53
+ const errors = [];
54
+ for (const rule of rules) {
55
+ const value = req.body[rule.field];
56
+ if (rule.required && (value === undefined || value === null || value === '')) {
57
+ errors.push(`${rule.field} is required`);
58
+ continue;
59
+ }
60
+ if (value === undefined || value === null)
61
+ continue;
62
+ if (rule.type) {
63
+ switch (rule.type) {
64
+ case 'email':
65
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
66
+ if (!emailRegex.test(value)) {
67
+ errors.push(`${rule.field} must be a valid email`);
68
+ }
69
+ break;
70
+ case 'string':
71
+ if (typeof value !== 'string') {
72
+ errors.push(`${rule.field} must be a string`);
73
+ }
74
+ break;
75
+ case 'number':
76
+ if (typeof value !== 'number' && isNaN(Number(value))) {
77
+ errors.push(`${rule.field} must be a number`);
78
+ }
79
+ break;
80
+ case 'boolean':
81
+ if (typeof value !== 'boolean') {
82
+ errors.push(`${rule.field} must be a boolean`);
83
+ }
84
+ break;
85
+ }
86
+ }
87
+ if (rule.minLength && value.length < rule.minLength) {
88
+ errors.push(`${rule.field} must be at least ${rule.minLength} characters`);
89
+ }
90
+ if (rule.maxLength && value.length > rule.maxLength) {
91
+ errors.push(`${rule.field} must be no more than ${rule.maxLength} characters`);
92
+ }
93
+ if (rule.pattern && !rule.pattern.test(value)) {
94
+ errors.push(`${rule.field} format is invalid`);
95
+ }
96
+ if (rule.custom) {
97
+ const result = rule.custom(value);
98
+ if (result !== true) {
99
+ errors.push(typeof result === 'string' ? result : `${rule.field} is invalid`);
100
+ }
101
+ }
102
+ }
103
+ if (errors.length > 0) {
104
+ return res.status(400).json({
105
+ success: false,
106
+ message: 'Validation failed',
107
+ data: undefined,
108
+ error: {
109
+ message: 'Validation failed',
110
+ details: errors
111
+ },
112
+ meta: null
113
+ });
114
+ }
115
+ next();
116
+ };
117
+ }
118
+ const rateLimitStore = new Map();
119
+ export function createRateLimitMiddleware(config = {}) {
120
+ const { windowMs = 15 * 60 * 1000, maxRequests = 100, message = 'Too many requests, please try again later', keyGenerator = (req) => req.ip || 'unknown' } = config;
121
+ return (req, res, next) => {
122
+ const key = keyGenerator(req);
123
+ const now = Date.now();
124
+ const record = rateLimitStore.get(key);
125
+ if (!record || now > record.resetTime) {
126
+ rateLimitStore.set(key, {
127
+ count: 1,
128
+ resetTime: now + windowMs
129
+ });
130
+ return next();
131
+ }
132
+ if (record.count >= maxRequests) {
133
+ return res.status(429).json({
134
+ success: false,
135
+ message,
136
+ data: undefined,
137
+ error: {
138
+ message,
139
+ details: {
140
+ retryAfter: Math.ceil((record.resetTime - now) / 1000)
141
+ }
142
+ },
143
+ meta: null
144
+ });
145
+ }
146
+ record.count++;
147
+ next();
148
+ };
149
+ }
150
+ export function createAuthMiddleware(config) {
151
+ const { tokenExtractor = (req) => {
152
+ const authHeader = req.headers.authorization;
153
+ if (authHeader && authHeader.startsWith('Bearer ')) {
154
+ return authHeader.substring(7);
155
+ }
156
+ return req.cookies?.token || null;
157
+ }, tokenValidator = () => { throw new Error('Token validator not implemented'); }, unauthorizedMessage = 'Unauthorized access' } = config;
158
+ return async (req, res, next) => {
159
+ try {
160
+ const token = tokenExtractor(req);
161
+ if (!token) {
162
+ return res.status(401).json({
163
+ success: false,
164
+ message: unauthorizedMessage,
165
+ data: undefined,
166
+ error: {
167
+ message: unauthorizedMessage
168
+ },
169
+ meta: null
170
+ });
171
+ }
172
+ const user = await tokenValidator(token);
173
+ req.user = user;
174
+ next();
175
+ }
176
+ catch (error) {
177
+ return res.status(401).json({
178
+ success: false,
179
+ message: unauthorizedMessage,
180
+ data: undefined,
181
+ error: {
182
+ message: unauthorizedMessage
183
+ },
184
+ meta: null
185
+ });
186
+ }
187
+ };
188
+ }
189
+ // Plugin versions
190
+ export function withLogging(format = 'simple') {
191
+ return (app) => {
192
+ app.use(createLoggingMiddleware(format));
193
+ };
194
+ }
195
+ export function withErrorHandler() {
196
+ return (app) => {
197
+ app.use(createErrorHandler());
198
+ };
199
+ }
200
+ export function withRequestId() {
201
+ return (app) => {
202
+ app.use(createRequestIdMiddleware());
203
+ };
204
+ }
205
+ export function withValidation(rules) {
206
+ return (app) => {
207
+ app.use(createValidationMiddleware(rules));
208
+ };
209
+ }
210
+ export function withRateLimit(config = {}) {
211
+ return (app) => {
212
+ app.use(createRateLimitMiddleware(config));
213
+ };
214
+ }
215
+ export function withAuth(config) {
216
+ return (app) => {
217
+ app.use(createAuthMiddleware(config));
218
+ };
219
+ }
220
+ // Convenience functions for route-specific middleware
221
+ export function validateFields(rules) {
222
+ return createValidationMiddleware(rules);
223
+ }
224
+ export function rateLimit(config = {}) {
225
+ return createRateLimitMiddleware(config);
226
+ }
227
+ export function requireAuth(config) {
228
+ return createAuthMiddleware(config);
229
+ }
230
+ // Cache response middleware (per-route opt-in)
231
+ export function cacheResponse(ttl) {
232
+ return async (req, res, next) => {
233
+ try {
234
+ if (req.method !== 'GET')
235
+ return next();
236
+ const cache = req.cache || req.app.locals.cache;
237
+ const defaultTTL = req.app.locals.cacheDefaultTTL;
238
+ if (!cache)
239
+ return next();
240
+ const key = `${req.originalUrl}`;
241
+ try {
242
+ const cached = await cache.get(key);
243
+ if (cached !== null && cached !== undefined) {
244
+ res.setHeader('X-Cache', 'HIT');
245
+ return res.json(cached);
246
+ }
247
+ }
248
+ catch (cacheErr) {
249
+ console.error(`[Cache] Failed to retrieve key "${key}":`, cacheErr);
250
+ // Continue without cache hit
251
+ }
252
+ const originalJson = res.json.bind(res);
253
+ res.json = (body) => {
254
+ try {
255
+ const expiry = ttl ?? defaultTTL;
256
+ if (expiry && cache) {
257
+ cache.set(key, body, expiry).catch((err) => {
258
+ console.error(`[Cache] Failed to set key "${key}" with TTL ${expiry}:`, err);
259
+ });
260
+ }
261
+ else if (cache) {
262
+ cache.set(key, body).catch((err) => {
263
+ console.error(`[Cache] Failed to set key "${key}":`, err);
264
+ });
265
+ }
266
+ }
267
+ catch (e) {
268
+ console.error(`[Cache] Error during cache.set operation:`, e);
269
+ }
270
+ res.setHeader('X-Cache', 'MISS');
271
+ return originalJson(body);
272
+ };
273
+ next();
274
+ }
275
+ catch (err) {
276
+ console.error('[Cache] Unexpected error in cacheResponse middleware:', err);
277
+ next();
278
+ }
279
+ };
280
+ }
281
+ // Session middleware helper (attaches sessionStore and helpers to req)
282
+ export function useSession(cookieName) {
283
+ return async (req, res, next) => {
284
+ try {
285
+ const store = req.app.locals.sessionStore;
286
+ if (!store)
287
+ return next();
288
+ const name = cookieName || req.app.locals.sessionCookieName || 'sid';
289
+ let sid = req.cookies?.[name] || req.cookies?.[name];
290
+ if (!sid) {
291
+ const cookieHeader = req.headers.cookie;
292
+ if (cookieHeader) {
293
+ const match = cookieHeader.split(';').map(s => s.trim()).find(s => s.startsWith(name + '='));
294
+ if (match)
295
+ sid = match.split('=')[1];
296
+ }
297
+ }
298
+ req.sessionId = sid;
299
+ req.sessionStore = store;
300
+ req.getSession = async () => {
301
+ if (!sid)
302
+ return null;
303
+ try {
304
+ return await store.get(sid);
305
+ }
306
+ catch (err) {
307
+ console.error(`[Session] Failed to get session "${sid}":`, err);
308
+ throw err;
309
+ }
310
+ };
311
+ req.createSession = async (id, data, ttl) => {
312
+ try {
313
+ return await store.create(id, data, ttl);
314
+ }
315
+ catch (err) {
316
+ console.error(`[Session] Failed to create session "${id}":`, err);
317
+ throw err;
318
+ }
319
+ };
320
+ next();
321
+ }
322
+ catch (err) {
323
+ console.error('[Session] Unexpected error in useSession middleware:', err);
324
+ next();
325
+ }
326
+ };
327
+ }
@@ -0,0 +1,11 @@
1
+ import { PeriodicHealthCheckConfig } from './types';
2
+ export declare class PeriodicHealthMonitor {
3
+ private intervals;
4
+ private config;
5
+ private serviceName;
6
+ constructor(config: PeriodicHealthCheckConfig, serviceName: string);
7
+ start(): void;
8
+ stop(): void;
9
+ private checkServiceHealth;
10
+ getHealthStatus(): Promise<Record<string, boolean>>;
11
+ }
@@ -0,0 +1,64 @@
1
+ export class PeriodicHealthMonitor {
2
+ constructor(config, serviceName) {
3
+ this.intervals = [];
4
+ this.config = config;
5
+ this.serviceName = serviceName;
6
+ }
7
+ start() {
8
+ if (!this.config.enabled || !this.config.services?.length) {
9
+ return;
10
+ }
11
+ const interval = this.config.interval || 30000;
12
+ this.config.services.forEach(service => {
13
+ const intervalId = setInterval(async () => {
14
+ await this.checkServiceHealth(service);
15
+ }, interval);
16
+ this.intervals.push(intervalId);
17
+ });
18
+ console.log(`📊 ${this.serviceName}: Periodic health monitoring enabled (${interval}ms interval) for ${this.config.services.length} service(s)`);
19
+ }
20
+ stop() {
21
+ this.intervals.forEach(interval => clearInterval(interval));
22
+ this.intervals = [];
23
+ console.log(`🛑 ${this.serviceName}: Periodic health monitoring stopped`);
24
+ }
25
+ async checkServiceHealth(service) {
26
+ try {
27
+ const controller = new AbortController();
28
+ const timeout = service.timeout || 5000;
29
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
30
+ const response = await fetch(service.url, {
31
+ method: 'GET',
32
+ signal: controller.signal,
33
+ headers: {
34
+ 'User-Agent': `${this.serviceName}-health-monitor`
35
+ }
36
+ });
37
+ clearTimeout(timeoutId);
38
+ if (response.ok) {
39
+ console.log(`🟢 ${service.name} is healthy`);
40
+ return true;
41
+ }
42
+ else {
43
+ console.log(`🔴 ${service.name} returned ${response.status}`);
44
+ return false;
45
+ }
46
+ }
47
+ catch (error) {
48
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
49
+ console.log(`🔴 ${service.name} health check failed: ${errorMessage}`);
50
+ return false;
51
+ }
52
+ }
53
+ // Get current health status of all services
54
+ async getHealthStatus() {
55
+ if (!this.config.services?.length) {
56
+ return {};
57
+ }
58
+ const results = {};
59
+ await Promise.all(this.config.services.map(async (service) => {
60
+ results[service.name] = await this.checkServiceHealth(service);
61
+ }));
62
+ return results;
63
+ }
64
+ }
@@ -0,0 +1,70 @@
1
+ import express from 'express';
2
+ import { Server } from 'http';
3
+ import { ServerConfig, SocketIOConfig } from './types';
4
+ export interface GrpcService {
5
+ service: Record<string, unknown>;
6
+ implementation: Record<string, (...args: unknown[]) => unknown>;
7
+ }
8
+ export interface RpcMethod {
9
+ [key: string]: (params: unknown[], callback: (error: Error | null, result?: unknown) => void) => void;
10
+ }
11
+ export interface WebhookConfig {
12
+ path: string;
13
+ secret?: string;
14
+ handler: (payload: Record<string, unknown>, headers: Record<string, string | string[]>) => void | Promise<void>;
15
+ }
16
+ export interface GrpcServerInstance {
17
+ start(): void;
18
+ forceShutdown(): void;
19
+ addService(service: unknown, implementation: unknown): void;
20
+ bindAsync(address: string, credentials: unknown, callback: () => void): void;
21
+ }
22
+ export interface ServerInstanceConfig extends Required<Omit<ServerConfig, 'socketIO' | 'name' | 'version'>> {
23
+ name: string;
24
+ version: string;
25
+ startTime: Date;
26
+ socketIO?: SocketIOConfig;
27
+ }
28
+ export interface ServerInstance {
29
+ app: express.Application;
30
+ server?: Server;
31
+ config: ServerInstanceConfig;
32
+ start(): Promise<ServerInstance>;
33
+ stop(): Promise<void>;
34
+ getInfo(): ServerInfo;
35
+ addGrpcService(service: Record<string, unknown>, implementation: Record<string, (...args: unknown[]) => unknown>, port?: number): void;
36
+ addRpcMethods(methods: RpcMethod, path?: string): void;
37
+ addWebhook(config: WebhookConfig): void;
38
+ addSocketIO(config?: SocketIOConfig): unknown;
39
+ }
40
+ export interface ServerInfo {
41
+ name: string;
42
+ version: string;
43
+ port: number;
44
+ uptime: number;
45
+ status: 'starting' | 'running' | 'stopping' | 'stopped';
46
+ startTime: Date;
47
+ }
48
+ export declare class ExpressServer implements ServerInstance {
49
+ app: express.Application;
50
+ server?: Server;
51
+ config: ServerInstanceConfig;
52
+ private status;
53
+ private grpcServices;
54
+ private grpcServer?;
55
+ private rpcMethods;
56
+ private socketIO?;
57
+ private healthMonitor?;
58
+ constructor(name?: string, version?: string, config?: ServerConfig);
59
+ private setupMiddleware;
60
+ private setupCacheAndSession;
61
+ private setupPeriodicHealthMonitoring;
62
+ start(): Promise<ServerInstance>;
63
+ stop(): Promise<void>;
64
+ getInfo(): ServerInfo;
65
+ addGrpcService(service: Record<string, unknown>, implementation: Record<string, (...args: unknown[]) => unknown>, port?: number): void;
66
+ addRpcMethods(methods: RpcMethod, path?: string): void;
67
+ addWebhook(config: WebhookConfig): void;
68
+ addSocketIO(config?: SocketIOConfig): unknown;
69
+ }
70
+ export declare function createServer(name?: string, version?: string, config?: ServerConfig): ServerInstance;