Skip to content

Commit 363fea1

Browse files
authored
feat: interceptor support (#631)
1 parent 77f74f9 commit 363fea1

12 files changed

+380
-21
lines changed

README.md

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -487,13 +487,13 @@ You can enable scoped controllers by providing a `scopedContainerGetter` functio
487487

488488
You will get a new instance for each event in the controller.
489489

490-
The `scopedContainerGetter` function receives a parameter which contains the socket, socket.io instance, event type, event name, namespace parameters and the message arguments if they are applicable.
490+
The `scopedContainerGetter` function receives the `SocketEventContext`.
491491

492492
The `scopedContainerDisposer` function receives the container instance you created with `scopedContainerGetter` after the socket action is finished. Use this function to dispose the container if needed.
493493

494494
```typescript
495495
import 'reflect-metadata';
496-
import { SocketControllers, ScopedContainerGetterParams } from 'socket-controllers';
496+
import { SocketControllers, SocketEventContext } from 'socket-controllers';
497497
import { Container, ContainerInstance, Token } from "typedi";
498498

499499
const myDiToken = new Token();
@@ -502,7 +502,7 @@ const myDiToken = new Token();
502502
const server = new SocketControllers({
503503
port: 3000,
504504
container: Container,
505-
scopedContainerGetter: (args: ScopedContainerGetterParams) => {
505+
scopedContainerGetter: (args: SocketEventContext) => {
506506
const container = Container.of(YOUR_REQUEST_CONTEXT);
507507
container.set(myDiToken, 'MY_VALUE');
508508
return container;
@@ -515,6 +515,56 @@ const server = new SocketControllers({
515515
});
516516
```
517517

518+
## Interceptors
519+
520+
Interceptors allow you to wrap your event handlers in higher order functions.
521+
With interceptors you can add logging or modify the incoming or outgoing data for event handlers.
522+
523+
```typescript
524+
import {
525+
SocketController,
526+
OnMessage,
527+
EmitOnSuccess,
528+
EmitOnFail,
529+
SkipEmitOnEmptyResult,
530+
UseInterceptor,
531+
MessageBody
532+
} from 'socket-controllers';
533+
534+
const interceptor: InterceptorInterface = {
535+
use: (ctx: SocketEventContext, next: () => any) => {
536+
ctx.messageArgs[0] = 'modified message from controller - ' + ctx.messageArgs[0];
537+
const resp = next();
538+
return 'modified response from controller - ' + resp; // modified response from controller - modified response from method - reponse
539+
},
540+
};
541+
542+
@Service()
543+
class Interceptor implements InterceptorInterface {
544+
async use(ctx: SocketEventContext, next: () => any) {
545+
ctx.messageArgs[0] = 'modified message from method - ' + ctx.messageArgs[0];
546+
const resp = await next();
547+
return 'modified response from method - ' + resp; // modified response from method - reponse
548+
}
549+
}
550+
551+
@SocketController()
552+
@UseInterceptor(interceptor)
553+
export class MessageController {
554+
@OnMessage('get')
555+
@EmitOnSuccess('get_success')
556+
@SkipEmitOnEmptyResult()
557+
@UseInterceptor(Interceptor)
558+
get(@MessageBody() message: string): Promise<Message[]> {
559+
console.log(message); // modified message from controller - modified message from method - original message
560+
return 'response';
561+
}
562+
}
563+
```
564+
565+
Interceptors are executed in order of definition, starting with the controller interceptors.
566+
567+
518568
## Decorators Reference
519569

520570
| Signature | Description |

src/SocketControllers.ts

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ import { TransformOptions } from './types/TransformOptions';
1818
import { defaultTransformOptions } from './types/constants/defaultTransformOptions';
1919
import { ActionTransformOptions } from './types/ActionTransformOptions';
2020
import { instanceToPlain, plainToInstance } from 'class-transformer';
21-
import { ScopedContainerGetterParams } from './types/ScopedContainerGetterParams';
2221
import { MiddlewareInterface } from './types/MiddlewareInterface';
22+
import { InterceptorInterface } from './types/InterceptorInterface';
23+
import { chainExecute } from './util/chain-execute';
24+
import { SocketEventContext } from './types/SocketEventContext';
2325

2426
export class SocketControllers {
2527
public container: { get<T>(someClass: { new (...args: any[]): T } | Function): T };
@@ -201,18 +203,44 @@ export class SocketControllers {
201203
data?: any[],
202204
ack?: Function | null
203205
) {
204-
const parameters = this.resolveParameters(socket, controller.metadata, action.parameters || [], data, ack);
206+
const eventContext = this.resolveEventContext(
207+
socket,
208+
action.type,
209+
eventName,
210+
data,
211+
controller.metadata.namespace,
212+
ack
213+
);
205214

206215
let container = this.container;
207216
if (this.options.scopedContainerGetter) {
208-
container = this.options.scopedContainerGetter(
209-
this.collectScopedContainerParams(socket, action.type, eventName, data, controller.metadata.namespace)
210-
);
217+
container = this.options.scopedContainerGetter(eventContext);
211218
}
212219

213220
try {
214221
const controllerInstance: any = container.get(controller.target);
215-
const actionResult = controllerInstance[action.methodName](...parameters);
222+
223+
const actions = [
224+
...(action.interceptors || []).map(interceptor => {
225+
return (
226+
((interceptor as any) instanceof Function
227+
? container.get(interceptor)
228+
: interceptor) as InterceptorInterface
229+
).use.bind(interceptor);
230+
}),
231+
(context: SocketEventContext) => {
232+
const parameters = this.resolveParameters(
233+
socket,
234+
controller.metadata,
235+
action.parameters || [],
236+
context.messageArgs,
237+
ack
238+
);
239+
return controllerInstance[action.methodName](...parameters);
240+
},
241+
];
242+
243+
const actionResult = chainExecute(eventContext, actions);
216244
const result = await Promise.resolve(actionResult);
217245
this.handleActionResult(socket, action, result, ResultType.EMIT_ON_SUCCESS);
218246
} catch (error: any) {
@@ -347,20 +375,22 @@ export class SocketControllers {
347375
return value;
348376
}
349377

350-
private collectScopedContainerParams(
378+
private resolveEventContext(
351379
socket: Socket,
352380
eventType: SocketEventType,
353381
eventName?: string,
354382
messageBody?: any[],
355-
namespace?: string | RegExp
356-
): ScopedContainerGetterParams {
383+
namespace?: string | RegExp,
384+
ack?: Function | null
385+
): SocketEventContext {
357386
return {
358387
eventType,
359388
eventName,
360389
socket,
361390
socketIo: this.io,
362391
nspParams: this.extractNamespaceParameters(socket, namespace),
363392
messageArgs: messageBody,
393+
ack,
364394
};
365395
}
366396

src/decorators/UseInterceptor.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { addInterceptorToActionMetadata } from '../util/add-interceptor-to-action-metadata';
2+
import { getMetadata } from '../util/get-metadata';
3+
import { ControllerMetadata } from '../types/ControllerMetadata';
4+
5+
export function UseInterceptor(...interceptors: any[]): Function {
6+
return function (object: Function | Object, methodName?: string) {
7+
for (const interceptor of interceptors) {
8+
if (object instanceof Function) {
9+
// Class interceptor
10+
const existingMetadata: ControllerMetadata = getMetadata(object);
11+
for (const key of Object.keys(existingMetadata?.actions || {})) {
12+
addInterceptorToActionMetadata(object, key, interceptor as Function);
13+
}
14+
} else {
15+
// Method interceptor
16+
addInterceptorToActionMetadata(object.constructor, methodName as string, interceptor as Function);
17+
}
18+
}
19+
};
20+
}

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ export * from './decorators/SocketRequest';
1818
export * from './decorators/SocketRooms';
1919

2020
export * from './types/MiddlewareInterface';
21+
export * from './types/InterceptorInterface';
2122
export * from './types/TransformOptions';
2223
export * from './types/SocketControllersOptions';
2324
export * from './types/enums/SocketEventType';
25+
export * from './types/SocketEventContext';
2426

2527
export * from './SocketControllers';

src/types/ActionMetadata.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ export interface ActionMetadata {
88
options: any;
99
parameters: ParameterMetadata[];
1010
results: ResultMetadata[];
11+
interceptors: Function[];
1112
}

src/types/InterceptorInterface.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { SocketEventContext } from './SocketEventContext';
2+
3+
export interface InterceptorInterface {
4+
use(context: SocketEventContext, next: () => any): any;
5+
}

src/types/SocketControllersOptions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { Server } from 'socket.io';
22
import { TransformOptions } from './TransformOptions';
3-
import { ScopedContainerGetterParams } from './ScopedContainerGetterParams';
3+
import { SocketEventContext } from './SocketEventContext';
44

55
export interface SocketControllersOptions {
66
container: { get<T>(someClass: { new (...args: any[]): T } | Function): T };
77

8-
scopedContainerGetter?: (params: ScopedContainerGetterParams) => {
8+
scopedContainerGetter?: (context: SocketEventContext) => {
99
get<T>(someClass: { new (...args: any[]): T } | Function): T;
1010
};
1111

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { SocketEventType } from './enums/SocketEventType';
22
import { Server, Socket } from 'socket.io';
33

4-
export interface ScopedContainerGetterParams {
4+
export interface SocketEventContext {
55
socketIo: Server;
66
socket: Socket;
77
eventType: SocketEventType;
88
eventName?: string;
99
messageArgs?: any[];
1010
nspParams?: Record<string, string>;
11+
ack?: Function | null;
1112
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { SOCKET_CONTROLLER_META_KEY } from '../types/SocketControllerMetaKey';
2+
import { getMetadata } from './get-metadata';
3+
import { ControllerMetadata } from '../types/ControllerMetadata';
4+
5+
export const addInterceptorToActionMetadata = (target: Function, methodName: string, interceptor: Function) => {
6+
const existingMetadata = getMetadata<any, ControllerMetadata>(target);
7+
(Reflect as any).defineMetadata(
8+
SOCKET_CONTROLLER_META_KEY,
9+
{
10+
...existingMetadata,
11+
actions: {
12+
...existingMetadata?.actions,
13+
[methodName]: {
14+
...existingMetadata?.actions?.[methodName],
15+
interceptors: [interceptor, ...(existingMetadata?.actions?.[methodName]?.interceptors || [])],
16+
},
17+
},
18+
},
19+
target
20+
);
21+
};

src/util/chain-execute.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export function chainExecute(context: any, chain: Function[]) {
2+
function next() {
3+
const middleware: Function = chain.shift() as Function;
4+
5+
if (middleware && typeof middleware === 'function') {
6+
return middleware(context, next);
7+
}
8+
}
9+
10+
return next();
11+
}

0 commit comments

Comments
 (0)