@naman_deep_singh/server-utils 1.0.0 → 1.0.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.
package/src/server.ts CHANGED
@@ -1,46 +1,372 @@
1
1
  import express from 'express';
2
- import cors from 'cors';
3
- import helmet from 'helmet';
4
- import { ServerConfig, ServerPlugin } from './types';
2
+ import { Server } from 'http';
3
+ import { ServerConfig, SocketIOConfig, SocketInstance } from './types';
4
+ import { createGracefulShutdown } from './shutdown';
5
+ import crypto from 'crypto';
5
6
 
6
- export function createServer(config: ServerConfig = {}): express.Application {
7
- const app = express();
7
+ export interface GrpcService {
8
+ service: Record<string, unknown>;
9
+ implementation: Record<string, (...args: unknown[]) => unknown>;
10
+ }
11
+
12
+ export interface RpcMethod {
13
+ [key: string]: (params: unknown[], callback: (error: Error | null, result?: unknown) => void) => void;
14
+ }
15
+
16
+ export interface WebhookConfig {
17
+ path: string;
18
+ secret?: string;
19
+ handler: (payload: Record<string, unknown>, headers: Record<string, string | string[]>) => void | Promise<void>;
20
+ }
21
+
22
+ export interface GrpcServerInstance {
23
+ start(): void;
24
+ forceShutdown(): void;
25
+ addService(service: unknown, implementation: unknown): void;
26
+ bindAsync(address: string, credentials: unknown, callback: () => void): void;
27
+ }
28
+
29
+
30
+
31
+ export interface ServerInstanceConfig extends Required<Omit<ServerConfig, 'socketIO' | 'name' | 'version'>> {
32
+ name: string;
33
+ version: string;
34
+ startTime: Date;
35
+ socketIO?: SocketIOConfig;
36
+ }
37
+
38
+ export interface ServerInstance {
39
+ app: express.Application;
40
+ server?: Server;
41
+ config: ServerInstanceConfig;
42
+ start(): Promise<ServerInstance>;
43
+ stop(): Promise<void>;
44
+ getInfo(): ServerInfo;
45
+
46
+ // Multi-protocol support
47
+ addGrpcService(service: Record<string, unknown>, implementation: Record<string, (...args: unknown[]) => unknown>, port?: number): void;
48
+ addRpcMethods(methods: RpcMethod, path?: string): void;
49
+ addWebhook(config: WebhookConfig): void;
50
+ addSocketIO(config?: SocketIOConfig): unknown;
51
+ }
52
+
53
+ export interface ServerInfo {
54
+ name: string;
55
+ version: string;
56
+ port: number;
57
+ uptime: number;
58
+ status: 'starting' | 'running' | 'stopping' | 'stopped';
59
+ startTime: Date;
60
+ }
61
+
62
+ export class ExpressServer implements ServerInstance {
63
+ public app: express.Application;
64
+ public server?: Server;
65
+ public config: ServerInstanceConfig;
66
+ private status: 'starting' | 'running' | 'stopping' | 'stopped' = 'stopped';
67
+ private grpcServices: GrpcService[] = [];
68
+ private grpcServer?: GrpcServerInstance;
69
+ private rpcMethods: RpcMethod = {};
70
+ private socketIO?: { close(): void };
71
+
72
+ constructor(
73
+ name: string = 'Express Server',
74
+ version: string = '1.0.0',
75
+ config: ServerConfig = {}
76
+ ) {
77
+ this.app = express();
78
+ this.config = {
79
+ name,
80
+ version,
81
+ startTime: new Date(),
82
+ port: config.port || 3000,
83
+ cors: config.cors ?? true,
84
+ helmet: config.helmet ?? true,
85
+ json: config.json ?? true,
86
+ customMiddleware: config.customMiddleware || [],
87
+ healthCheck: config.healthCheck ?? true,
88
+ gracefulShutdown: config.gracefulShutdown ?? true,
89
+ socketIO: config.socketIO
90
+ };
91
+
92
+ // Apply middleware based on configuration
93
+ this.setupMiddleware();
94
+ }
95
+
96
+ private setupMiddleware(): void {
97
+ // Apply CORS if enabled
98
+ if (this.config.cors) {
99
+ try {
100
+ const cors = require('cors');
101
+ const corsOptions = typeof this.config.cors === 'object' ? this.config.cors : undefined;
102
+ this.app.use(cors(corsOptions));
103
+ } catch (error) {
104
+ console.warn('CORS middleware not available. Install cors package.');
105
+ }
106
+ }
107
+
108
+ // Apply Helmet if enabled
109
+ if (this.config.helmet) {
110
+ try {
111
+ const helmet = require('helmet');
112
+ this.app.use(helmet());
113
+ } catch (error) {
114
+ console.warn('Helmet middleware not available. Install helmet package.');
115
+ }
116
+ }
117
+
118
+ // Apply JSON parser if enabled
119
+ if (this.config.json) {
120
+ this.app.use(express.json());
121
+ }
122
+
123
+ // Apply custom middleware
124
+ if (this.config.customMiddleware && this.config.customMiddleware.length > 0) {
125
+ this.config.customMiddleware.forEach(middleware => {
126
+ this.app.use(middleware);
127
+ });
128
+ }
129
+
130
+ // Add health check if enabled
131
+ if (this.config.healthCheck) {
132
+ const healthPath = typeof this.config.healthCheck === 'string' ? this.config.healthCheck : '/health';
133
+ this.app.get(healthPath, (req, res) => {
134
+ res.status(200).json({
135
+ status: 'healthy',
136
+ service: this.config.name,
137
+ version: this.config.version,
138
+ uptime: Date.now() - this.config.startTime.getTime(),
139
+ timestamp: new Date().toISOString()
140
+ });
141
+ });
142
+ }
143
+ }
144
+
145
+ async start(): Promise<ServerInstance> {
146
+ this.status = 'starting';
147
+
148
+ return new Promise((resolve, reject) => {
149
+ try {
150
+ this.server = this.app.listen(this.config.port, () => {
151
+ this.status = 'running';
152
+ console.log(`🚀 ${this.config.name} v${this.config.version} running on http://localhost:${this.config.port}`);
153
+
154
+ if (this.config.gracefulShutdown) {
155
+ createGracefulShutdown(this.server!, {
156
+ onShutdown: async () => {
157
+ this.status = 'stopping';
158
+ }
159
+ });
160
+ }
161
+
162
+ resolve(this);
163
+ });
8
164
 
9
- // Apply default middleware
10
- if (config.helmet !== false) {
11
- app.use(helmet());
165
+ this.server.on('error', reject);
166
+ } catch (error: unknown) {
167
+ this.status = 'stopped';
168
+ reject(error);
169
+ }
170
+ });
12
171
  }
13
172
 
14
- if (config.cors !== false) {
15
- const corsOptions = typeof config.cors === 'object' ? config.cors : undefined;
16
- app.use(cors(corsOptions));
173
+ async stop(): Promise<void> {
174
+ this.status = 'stopping';
175
+
176
+ // Stop gRPC server if running
177
+ if (this.grpcServer) {
178
+ this.grpcServer.forceShutdown();
179
+ }
180
+
181
+ // Stop Socket.IO server if running
182
+ if (this.socketIO) {
183
+ this.socketIO.close();
184
+ }
185
+
186
+ if (!this.server) {
187
+ this.status = 'stopped';
188
+ return;
189
+ }
190
+
191
+ return new Promise((resolve) => {
192
+ this.server!.close(() => {
193
+ this.status = 'stopped';
194
+ console.log(`👋 ${this.config.name} stopped`);
195
+ resolve();
196
+ });
197
+ });
17
198
  }
18
199
 
19
- if (config.json !== false) {
20
- app.use(express.json());
200
+ getInfo(): ServerInfo {
201
+ return {
202
+ name: this.config.name,
203
+ version: this.config.version,
204
+ port: this.config.port,
205
+ uptime: Date.now() - this.config.startTime.getTime(),
206
+ status: this.status,
207
+ startTime: this.config.startTime
208
+ };
21
209
  }
22
210
 
23
- // Apply custom middleware
24
- if (config.customMiddleware) {
25
- config.customMiddleware.forEach(middleware => {
26
- app.use(middleware);
211
+ addGrpcService(service: Record<string, unknown>, implementation: Record<string, (...args: unknown[]) => unknown>, port: number = 50051): void {
212
+ this.grpcServices.push({ service, implementation });
213
+
214
+ // Lazy load gRPC to avoid dependency issues
215
+ if (!this.grpcServer) {
216
+ try {
217
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
218
+ const grpc = require('@grpc/grpc-js') as {
219
+ Server: new () => {
220
+ start(): void;
221
+ forceShutdown(): void;
222
+ addService(service: unknown, implementation: unknown): void;
223
+ bindAsync(address: string, credentials: unknown, callback: () => void): void;
224
+ };
225
+ ServerCredentials: { createInsecure(): unknown };
226
+ };
227
+ this.grpcServer = new grpc.Server();
228
+
229
+ // Add all services
230
+ this.grpcServices.forEach(({ service, implementation }) => {
231
+ this.grpcServer!.addService(service, implementation);
232
+ });
233
+
234
+ this.grpcServer.bindAsync(
235
+ `0.0.0.0:${port}`,
236
+ grpc.ServerCredentials.createInsecure(),
237
+ () => {
238
+ this.grpcServer!.start();
239
+ console.log(`🔗 gRPC server running on port ${port}`);
240
+ }
241
+ );
242
+ } catch (error: unknown) {
243
+ console.warn('gRPC not available. Install @grpc/grpc-js to use gRPC features.');
244
+ }
245
+ }
246
+ }
247
+
248
+ addRpcMethods(methods: RpcMethod, path: string = '/rpc'): void {
249
+ Object.assign(this.rpcMethods, methods);
250
+
251
+ try {
252
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
253
+ const jayson = require('jayson') as {
254
+ server: (methods: RpcMethod) => {
255
+ middleware(): express.RequestHandler;
256
+ };
257
+ };
258
+ const rpcServer = jayson.server(this.rpcMethods);
259
+ this.app.use(path, rpcServer.middleware());
260
+ console.log(`📡 JSON-RPC server mounted on ${path}`);
261
+ } catch (error: unknown) {
262
+ console.warn('JSON-RPC not available. Install jayson to use RPC features.');
263
+ }
264
+ }
265
+
266
+ addWebhook(config: WebhookConfig): void {
267
+ this.app.post(config.path, express.raw({ type: 'application/json' }), async (req, res) => {
268
+ try {
269
+ // Verify signature if secret provided
270
+ if (config.secret) {
271
+ const signature = req.headers['x-hub-signature-256'] || req.headers['x-signature-256'];
272
+ if (signature) {
273
+ const expectedSignature = crypto
274
+ .createHmac('sha256', config.secret)
275
+ .update(req.body)
276
+ .digest('hex');
277
+
278
+ const providedSignature = Array.isArray(signature) ? signature[0] : signature;
279
+ if (!providedSignature.includes(expectedSignature)) {
280
+ return res.status(401).json({ error: 'Invalid signature' });
281
+ }
282
+ }
283
+ }
284
+
285
+ // Parse JSON payload
286
+ const payload = JSON.parse(req.body.toString());
287
+
288
+ // Call handler
289
+ await config.handler(payload, req.headers as Record<string, string | string[]>);
290
+
291
+ res.status(200).json({ success: true });
292
+ } catch (error: unknown) {
293
+ console.error('Webhook error:', error);
294
+ res.status(500).json({ error: 'Webhook processing failed' });
295
+ }
27
296
  });
297
+
298
+ console.log(`🪝 Webhook registered at ${config.path}`);
28
299
  }
29
300
 
30
- return app;
31
- }
301
+ addSocketIO(config: SocketIOConfig = {}): unknown {
302
+ if (!this.server) {
303
+ throw new Error('Server must be started before adding Socket.IO');
304
+ }
32
305
 
33
- export function withPlugin(app: express.Application, plugin: ServerPlugin, config: ServerConfig = {}): express.Application {
34
- plugin(app, config);
35
- return app;
36
- }
306
+ try {
307
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
308
+ const { Server } = require('socket.io') as {
309
+ Server: new (server: Server, options?: {
310
+ cors?: {
311
+ origin?: string | string[] | boolean;
312
+ methods?: string[];
313
+ credentials?: boolean;
314
+ };
315
+ path?: string;
316
+ }) => {
317
+ on: (event: string, handler: (socket: unknown) => void) => void;
318
+ close: () => void;
319
+ };
320
+ };
321
+
322
+ // Configure CORS
323
+ const corsConfig = config.cors === true
324
+ ? { origin: '*', methods: ['GET', 'POST'] }
325
+ : config.cors || undefined;
326
+
327
+ // Create Socket.IO server
328
+ const io = new Server(this.server, {
329
+ cors: config.cors ? corsConfig : undefined,
330
+ path: config.path || '/socket.io'
331
+ });
37
332
 
38
- export function createServerWithPlugins(config: ServerConfig = {}, ...plugins: ServerPlugin[]): express.Application {
39
- const app = createServer(config);
40
-
41
- plugins.forEach(plugin => {
42
- plugin(app, config);
43
- });
333
+ // Store reference for cleanup
334
+ this.socketIO = io;
335
+
336
+ // Handle connections
337
+ io.on('connection', (socket: unknown) => {
338
+ const typedSocket = socket as SocketInstance;
339
+ console.log(`🔌 Socket connected: ${typedSocket.id}`);
340
+
341
+ // Call user-defined connection handler
342
+ if (config.onConnection) {
343
+ config.onConnection(socket);
344
+ }
345
+
346
+ // Handle disconnection
347
+ typedSocket.on('disconnect', (reason) => {
348
+ console.log(`🔌 Socket disconnected: ${typedSocket.id} - ${reason}`);
349
+
350
+ // Call user-defined disconnection handler
351
+ if (config.onDisconnection) {
352
+ config.onDisconnection(socket, reason as string);
353
+ }
354
+ });
355
+ });
356
+
357
+ console.log(`🔌 Socket.IO server attached${config.path ? ` at ${config.path}` : ''}`);
358
+ return io;
359
+ } catch (error: unknown) {
360
+ console.warn('Socket.IO not available. Install socket.io to use WebSocket features.');
361
+ return null;
362
+ }
363
+ }
364
+ }
44
365
 
45
- return app;
366
+ export function createServer(
367
+ name?: string,
368
+ version?: string,
369
+ config?: ServerConfig
370
+ ): ServerInstance {
371
+ return new ExpressServer(name, version, config);
46
372
  }
package/src/types.ts CHANGED
@@ -1,5 +1,15 @@
1
1
  import express from 'express';
2
- import { CorsOptions } from 'cors';
2
+
3
+ export interface CorsOptions {
4
+ origin?: string | string[] | boolean | RegExp | ((origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => void);
5
+ methods?: string | string[];
6
+ allowedHeaders?: string | string[];
7
+ exposedHeaders?: string | string[];
8
+ credentials?: boolean;
9
+ maxAge?: number;
10
+ preflightContinue?: boolean;
11
+ optionsSuccessStatus?: number;
12
+ }
3
13
 
4
14
  export interface ServerConfig {
5
15
  port?: number;
@@ -9,6 +19,21 @@ export interface ServerConfig {
9
19
  customMiddleware?: express.RequestHandler[];
10
20
  healthCheck?: boolean | string;
11
21
  gracefulShutdown?: boolean;
22
+ socketIO?: SocketIOConfig;
23
+ name?: string;
24
+ version?: string;
25
+ }
26
+
27
+ export interface SocketIOConfig {
28
+ enabled?: boolean;
29
+ cors?: boolean | {
30
+ origin?: string | string[] | boolean;
31
+ methods?: string[];
32
+ credentials?: boolean;
33
+ };
34
+ onConnection?: (socket: unknown) => void;
35
+ onDisconnection?: (socket: unknown, reason: string) => void;
36
+ path?: string;
12
37
  }
13
38
 
14
39
  export interface HealthCheckConfig {
@@ -26,4 +51,14 @@ export interface GracefulShutdownConfig {
26
51
  onShutdown?: () => Promise<void>;
27
52
  }
28
53
 
54
+ export interface SocketInstance {
55
+ id: string;
56
+ emit: (event: string, data?: unknown) => void;
57
+ on: (event: string, handler: (...args: unknown[]) => void) => void;
58
+ broadcast: {
59
+ emit: (event: string, data?: unknown) => void;
60
+ };
61
+ disconnect: () => void;
62
+ }
63
+
29
64
  export type ServerPlugin = (app: express.Application, config: ServerConfig) => void;
package/tsconfig.json CHANGED
@@ -3,14 +3,18 @@
3
3
  "target": "ES2020",
4
4
  "module": "CommonJS",
5
5
  "moduleResolution": "node",
6
- "outDir": "./dist",
7
6
  "rootDir": "./src",
7
+ "outDir": "./dist",
8
8
  "strict": true,
9
9
  "esModuleInterop": true,
10
10
  "allowSyntheticDefaultImports": true,
11
11
  "skipLibCheck": true,
12
12
  "forceConsistentCasingInFileNames": true,
13
- "declaration": true
13
+ "declaration": true,
14
+ "baseUrl": ".",
15
+ "paths": {
16
+ "*": ["*", "*.ts", "*.js"]
17
+ }
14
18
  },
15
19
  "include": ["src/**/*"],
16
20
  "exclude": ["node_modules", "dist"]