@funduck/connectrpc-fastify 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/package.json CHANGED
@@ -1,14 +1,13 @@
1
1
  {
2
2
  "name": "@funduck/connectrpc-fastify",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "author": "Oleg Milekhin <qlfunduck@gmail.com>",
5
5
  "description": "Wrapper for official @connectrpc/connect and fastify. Simplifies configuration, type safe binding to controller, simplifies use of middlewares and guards.",
6
- "main": "dist/index.js",
6
+ "main": "dist/src/index.js",
7
7
  "scripts": {
8
- "build": "tsc -p tsconfig.build.json",
8
+ "build": "npx rimraf dist; tsc -p tsconfig.build.json",
9
9
  "compile-proto": "cd test && npx buf dep update; npx buf lint; npx buf generate",
10
- "demo-test": "cd test && DEBUG=true npx tsx e2e-demo.ts",
11
- "test": "echo \"Error: no test specified\" && exit 1"
10
+ "test": "cd test && DEBUG=true npx tsx e2e-demo.ts"
12
11
  },
13
12
  "license": "MIT",
14
13
  "dependencies": {
package/.editorconfig DELETED
@@ -1,12 +0,0 @@
1
- root = true
2
-
3
- [*]
4
- charset = utf-8
5
-
6
- [Makefile]
7
- indent_style = tab
8
-
9
- [*.{ts,js,json,yml,yaml}]
10
- indent_style = space
11
- indent_size = 2
12
- trim_trailing_whitespace = true
package/.prettierrc DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "singleQuote": true,
3
- "trailingComma": "all",
4
- "plugins": [
5
- "prettier-plugin-organize-imports"
6
- ]
7
- }
@@ -1,2 +0,0 @@
1
- ignoredBuiltDependencies:
2
- - '@bufbuild/buf'
package/publish.sh DELETED
@@ -1,3 +0,0 @@
1
- #!/bin/sh
2
-
3
- npm publish --tag beta --access public
package/src/connectrpc.ts DELETED
@@ -1,87 +0,0 @@
1
- import { GenService, GenServiceMethods } from '@bufbuild/protobuf/codegenv2';
2
- import { FastifyInstance } from 'fastify';
3
- import { registerFastifyPlugin } from './fastify-plugin';
4
- import { initGuards } from './guards';
5
- import { setLogger } from './helpers';
6
- import {
7
- Guard,
8
- Logger,
9
- Middleware,
10
- MiddlewareConfigUnion,
11
- Service,
12
- } from './interfaces';
13
- import { initMiddlewares } from './middlewares';
14
- import { ControllersStore, GuardsStore, MiddlewareStore } from './stores';
15
-
16
- class ConnectRPCClass {
17
- setLogger(customLogger: Logger) {
18
- setLogger(customLogger);
19
- }
20
-
21
- registerMiddleware(
22
- self: Middleware,
23
- options?: {
24
- allowMultipleInstances?: boolean;
25
- },
26
- ) {
27
- MiddlewareStore.registerInstance(self, options);
28
- }
29
-
30
- /**
31
- * @param self - instance of controller
32
- * @param service - generated service that is implemented by controller
33
- */
34
- registerController<T extends GenServiceMethods>(
35
- self: Service<GenService<T>>,
36
- service: GenService<T>,
37
- options?: {
38
- allowMultipleInstances?: boolean;
39
- },
40
- ) {
41
- ControllersStore.registerInstance(self, service, options);
42
- }
43
-
44
- registerGuard(
45
- self: Guard,
46
- options?: {
47
- allowMultipleInstances?: boolean;
48
- },
49
- ) {
50
- GuardsStore.registerInstance(self, options);
51
- }
52
-
53
- registerFastifyPlugin(server: FastifyInstance) {
54
- return registerFastifyPlugin(server);
55
- }
56
-
57
- private _middlewaresInitialized = false;
58
-
59
- initMiddlewares(
60
- server: FastifyInstance,
61
- middlewareConfigs: MiddlewareConfigUnion[],
62
- ) {
63
- if (this._middlewaresInitialized) {
64
- throw new Error('Middlewares have already been initialized!');
65
- }
66
- if (this._guardsInitialized) {
67
- throw new Error('Middlewares must be initialized before guards!');
68
- }
69
- this._middlewaresInitialized = true;
70
- return initMiddlewares(server, middlewareConfigs);
71
- }
72
-
73
- private _guardsInitialized = false;
74
-
75
- initGuards(server: FastifyInstance) {
76
- if (this._guardsInitialized) {
77
- throw new Error('Guards have already been initialized!');
78
- }
79
- this._guardsInitialized = true;
80
- return initGuards(server);
81
- }
82
- }
83
-
84
- /**
85
- * Main ConnectRPC class to manage registration of controllers and middlewares
86
- */
87
- export const ConnectRPC = new ConnectRPCClass();
@@ -1,45 +0,0 @@
1
- import { FastifyReply, FastifyRequest } from 'fastify';
2
- import { ExecutionContext, Type } from './interfaces';
3
-
4
- export class ManualExecutionContext implements ExecutionContext {
5
- constructor(
6
- readonly request: FastifyRequest['raw'],
7
- readonly response: FastifyReply['raw'],
8
- readonly next: <T = any>() => T,
9
- readonly args: any[],
10
- readonly constructorRef: Type<any> | null = null,
11
- readonly handler: Function | null = null,
12
- ) {}
13
-
14
- getClass<T = any>(): Type<T> {
15
- return this.constructorRef!;
16
- }
17
-
18
- getHandler(): Function {
19
- return this.handler!;
20
- }
21
-
22
- getArgs<T extends Array<any> = any[]>(): T {
23
- return this.args as T;
24
- }
25
-
26
- getArgByIndex<T = any>(index: number): T {
27
- return this.args[index] as T;
28
- }
29
-
30
- switchToHttp() {
31
- return Object.assign(this, {
32
- getRequest: () => this.request,
33
- getResponse: () => this.response,
34
- getNext: () => this.next,
35
- });
36
- }
37
-
38
- switchToRpc() {
39
- throw new Error('Context switching to RPC is not supported.');
40
- }
41
-
42
- switchToWs() {
43
- throw new Error('Context switching to WebSockets is not supported.');
44
- }
45
- }
@@ -1,99 +0,0 @@
1
- import { GenService } from '@bufbuild/protobuf/codegenv2';
2
- import { ConnectRouter } from '@connectrpc/connect';
3
- import { fastifyConnectPlugin } from '@connectrpc/connect-fastify';
4
- import { Compression } from '@connectrpc/connect/protocol';
5
- import { FastifyInstance } from 'fastify';
6
- import { getGuards } from './guards';
7
- import { discoverMethodMappings, logger } from './helpers';
8
- import { ControllersStore, RouteMetadataStore } from './stores';
9
-
10
- export async function registerFastifyPlugin(
11
- server: FastifyInstance,
12
- options: {
13
- acceptCompression?: Compression[];
14
- } = {},
15
- ) {
16
- // Create implementations from controller instances
17
- const implementations = new Map<GenService<any>, any>();
18
-
19
- for (const { instance, service } of ControllersStore.values()) {
20
- const guards = getGuards(instance);
21
- if (guards.length > 0) {
22
- logger.log(
23
- `Found ${guards.length} guards on controller ${instance.constructor.name}`,
24
- );
25
- }
26
-
27
- const methodMappings = discoverMethodMappings(instance.__proto__, service);
28
-
29
- // Create the implementation object
30
- const implementation: any = {};
31
-
32
- // Bind each method from the service
33
- for (const methodDesc of service.methods) {
34
- const { name } = methodDesc;
35
- const methodName = name[0].toLowerCase() + name.slice(1);
36
-
37
- // Check if there's a mapped controller method
38
- const controllerMethodName = methodMappings[name];
39
-
40
- if (controllerMethodName) {
41
- const controllerMethod = instance[controllerMethodName];
42
-
43
- if (controllerMethod) {
44
- // Bind the method with proper 'this' context
45
- const bindedMethod = controllerMethod.bind(instance);
46
- implementation[methodName] = (...args: any[]) => {
47
- return bindedMethod(...args);
48
- };
49
-
50
- // Store route metadata for guards and interceptors
51
- RouteMetadataStore.registerRoute(
52
- service.typeName,
53
- name, // PascalCase method name (e.g., "Say")
54
- instance.constructor,
55
- controllerMethod,
56
- controllerMethodName,
57
- instance,
58
- );
59
-
60
- logger.log(
61
- `Binding ${instance.constructor.name}.${controllerMethodName} to ${service.typeName}.${name}`,
62
- );
63
- } else {
64
- logger.warn(
65
- `Method ${controllerMethodName} not found in ${instance.constructor.name}`,
66
- );
67
- }
68
- }
69
- }
70
-
71
- implementations.set(service, implementation);
72
- }
73
-
74
- const routes = (router: ConnectRouter) => {
75
- for (const [service, implementation] of implementations.entries()) {
76
- router.service(service, implementation);
77
- logger.log(`Registered {/${service.typeName}} route`);
78
- }
79
- };
80
-
81
- if (routes.length === 0) {
82
- logger.warn('No controllers found to register');
83
- return;
84
- }
85
-
86
- await server.register(fastifyConnectPlugin, {
87
- // For now we enable only Connect protocol by default and disable others.
88
- // grpc: this.options.grpc ?? false,
89
- // grpcWeb: this.options.grpcWeb ?? false,
90
- // connect: this.options.connect ?? true,
91
- grpc: false,
92
- grpcWeb: false,
93
- connect: true,
94
- acceptCompression: options.acceptCompression ?? [],
95
- routes: routes,
96
- });
97
-
98
- logger.log('Ready');
99
- }
package/src/guards.ts DELETED
@@ -1,130 +0,0 @@
1
- import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
2
- import { ManualExecutionContext } from './execution-context';
3
- import { logger } from './helpers';
4
- import { ExecutionContext, Guard } from './interfaces';
5
- import { GuardsStore, RouteMetadataStore } from './stores';
6
-
7
- export class ManualGuardExecutor {
8
- async executeGuard(
9
- guard: Guard,
10
- context: ExecutionContext,
11
- ): Promise<boolean> {
12
- const result = guard.canActivate(context);
13
-
14
- // Handle synchronous boolean result
15
- if (typeof result === 'boolean') {
16
- return result;
17
- }
18
-
19
- // Handle Promise result
20
- return await result;
21
- }
22
-
23
- async executeGuards(
24
- guards: Guard[],
25
- context: ExecutionContext,
26
- ): Promise<boolean> {
27
- for (const guard of guards) {
28
- const canActivate = await this.executeGuard(guard, context);
29
- if (!canActivate) {
30
- return false;
31
- }
32
- }
33
- return true;
34
- }
35
- }
36
-
37
- export function getGuards(controller: any): Guard[] {
38
- // Return all registered guards
39
- // In a more sophisticated implementation, you could filter guards
40
- // based on decorators on the controller or method
41
- return GuardsStore.getAllGuards();
42
- }
43
-
44
- /**
45
- * Initialize guards middleware - this should be called after all other middlewares are registered
46
- */
47
- export async function initGuards(server: FastifyInstance) {
48
- const guardExecutor = new ManualGuardExecutor();
49
-
50
- // Add a hook that runs after all other middlewares
51
- server.addHook(
52
- 'preHandler',
53
- async (request: FastifyRequest, reply: FastifyReply) => {
54
- const url = request.url;
55
-
56
- // Parse the URL to get the route
57
- // Format: /package.ServiceName/MethodName
58
- const match = url.match(/^\/([^/]+)\/([^/]+)$/);
59
-
60
- if (!match) {
61
- // Not a ConnectRPC route, skip guard execution
62
- return;
63
- }
64
-
65
- // Get route metadata
66
- const routeMetadata = RouteMetadataStore.getRouteMetadata(url);
67
-
68
- if (!routeMetadata) {
69
- // No metadata found for this route
70
- logger.warn(`No route metadata found for ${url}`);
71
- return;
72
- }
73
-
74
- const {
75
- controllerClass,
76
- controllerMethod,
77
- controllerMethodName,
78
- instance,
79
- } = routeMetadata;
80
-
81
- // Get guards for the controller
82
- const guards = getGuards(instance);
83
-
84
- if (guards.length === 0) {
85
- // No guards to execute
86
- return;
87
- }
88
-
89
- // Create execution context
90
- // Note: For ConnectRPC, we don't have the actual request arguments yet at this point
91
- // They will be deserialized later by the ConnectRPC handler
92
- const executionContext = new ManualExecutionContext(
93
- request.raw,
94
- reply.raw,
95
- (() => undefined) as <T = any>() => T,
96
- [], // args will be populated later if needed
97
- controllerClass,
98
- controllerMethod,
99
- );
100
-
101
- // Execute guards
102
- try {
103
- const canActivate = await guardExecutor.executeGuards(
104
- guards,
105
- executionContext,
106
- );
107
-
108
- if (!canActivate) {
109
- // Guard rejected the request
110
- reply.code(403).send({
111
- code: 'permission_denied',
112
- message: 'Forbidden',
113
- });
114
- throw new Error('Guard rejected the request');
115
- }
116
- } catch (error) {
117
- // If guard throws an error, reject the request
118
- if (!reply.sent) {
119
- reply.code(403).send({
120
- code: 'permission_denied',
121
- message: error instanceof Error ? error.message : 'Forbidden',
122
- });
123
- }
124
- throw error;
125
- }
126
- },
127
- );
128
-
129
- logger.log('Guards middleware initialized');
130
- }
package/src/helpers.ts DELETED
@@ -1,92 +0,0 @@
1
- import { GenService } from '@bufbuild/protobuf/codegenv2';
2
- import { FastifyReply, FastifyRequest } from 'fastify';
3
- import { Logger } from './interfaces';
4
-
5
- /**
6
- * Automatically discover method mappings by matching service methods to controller methods
7
- */
8
- export function discoverMethodMappings(
9
- prototype,
10
- service: GenService<any>,
11
- ): Record<string, string> {
12
- const methodMappings: Record<string, string> = {};
13
-
14
- // service.methods is an array of method descriptors
15
- const serviceMethods = Array.isArray(service.methods) ? service.methods : [];
16
-
17
- // Get all controller methods
18
- const controllerMethods = Object.getOwnPropertyNames(prototype).filter(
19
- (name) => name !== 'constructor' && typeof prototype[name] === 'function',
20
- );
21
-
22
- // Check each service method
23
- for (const methodDesc of serviceMethods) {
24
- const serviceMethodName = methodDesc.name; // e.g., "Say" - this is what connectrpc module uses as key
25
- const localName = methodDesc.localName; // e.g., "say" (camelCase version)
26
-
27
- // Try to find a matching controller method
28
- // First try exact match with localName (camelCase), then try case-insensitive
29
- let controllerMethodName = controllerMethods.find(
30
- (name) => name === localName,
31
- );
32
-
33
- if (!controllerMethodName) {
34
- controllerMethodName = controllerMethods.find(
35
- (name) => name.toLowerCase() === serviceMethodName.toLowerCase(),
36
- );
37
- }
38
-
39
- if (controllerMethodName) {
40
- // Map using the service method name (e.g., "Say") because that's what the module looks for
41
- methodMappings[serviceMethodName] = controllerMethodName;
42
- }
43
- }
44
-
45
- return methodMappings;
46
- }
47
-
48
- /**
49
- * Helper to convert NestJS middleware to Fastify hook
50
- */
51
- export function convertMiddlewareToHook(
52
- middlewareInstance: any,
53
- ): (request: FastifyRequest, reply: FastifyReply) => Promise<void> {
54
- return async (request: FastifyRequest, reply: FastifyReply) => {
55
- return new Promise<void>((resolve, reject) => {
56
- try {
57
- // NestJS middleware expects raw req/res
58
- middlewareInstance.use(request.raw, reply.raw, (err?: any) => {
59
- if (err) {
60
- reject(err);
61
- } else {
62
- resolve();
63
- }
64
- });
65
- } catch (error) {
66
- reject(error);
67
- }
68
- });
69
- };
70
- }
71
-
72
- export let logger: Logger = {
73
- log: (...args: any[]) => {
74
- console.info(...args);
75
- },
76
- error: (...args: any[]) => {
77
- console.error(...args);
78
- },
79
- warn: (...args: any[]) => {
80
- console.warn(...args);
81
- },
82
- debug: (...args: any[]) => {
83
- console.debug(...args);
84
- },
85
- verbose: (...args: any[]) => {
86
- console.log(...args);
87
- },
88
- };
89
-
90
- export function setLogger(customLogger: Logger) {
91
- logger = customLogger;
92
- }
package/src/index.ts DELETED
@@ -1,21 +0,0 @@
1
- export function printMsg() {
2
- console.log('Thanks for using connectrpc-fastify!');
3
- }
4
-
5
- export { ConnectRPC } from './connectrpc';
6
-
7
- export { middlewareConfig } from './interfaces';
8
-
9
- export type {
10
- ExecutionContext,
11
- Guard,
12
- Logger,
13
- Middleware,
14
- MiddlewareConfig,
15
- MiddlewareConfigUnion,
16
- Service,
17
- } from './interfaces';
18
-
19
- export { initGuards } from './guards';
20
-
21
- export type { OmitConnectrpcFields } from './types';
package/src/interfaces.ts DELETED
@@ -1,175 +0,0 @@
1
- import type { GenMessage, GenService } from '@bufbuild/protobuf/codegenv2';
2
- import { FastifyReply, FastifyRequest } from 'fastify';
3
- import { OmitConnectrpcFields } from './types';
4
-
5
- export interface Logger {
6
- log: (...args: any[]) => void;
7
- error: (...args: any[]) => void;
8
- warn: (...args: any[]) => void;
9
- debug: (...args: any[]) => void;
10
- verbose: (...args: any[]) => void;
11
- }
12
-
13
- export interface Middleware {
14
- use(
15
- req: FastifyRequest['raw'],
16
- res: FastifyReply['raw'],
17
- next: (err?: any) => void,
18
- ): void;
19
- }
20
-
21
- export interface Type<T = any> extends Function {
22
- new (...args: any[]): T;
23
- }
24
-
25
- /**
26
- * Extract the input type from a method schema
27
- */
28
- type ExtractInput<T> = T extends { input: GenMessage<infer M> } ? M : never;
29
-
30
- /**
31
- * Extract the output type from a method schema
32
- */
33
- type ExtractOutput<T> = T extends { output: GenMessage<infer M> } ? M : never;
34
-
35
- /**
36
- * Convert a service method to a controller method signature
37
- */
38
- type ServiceMethod<T> = T extends { methodKind: 'unary' }
39
- ? (
40
- request: ExtractInput<T>,
41
- ) => Promise<OmitConnectrpcFields<ExtractOutput<T>>>
42
- : T extends { methodKind: 'server_streaming' }
43
- ? (
44
- request: ExtractInput<T>,
45
- ) => AsyncIterable<OmitConnectrpcFields<ExtractOutput<T>>>
46
- : T extends { methodKind: 'client_streaming' }
47
- ? (
48
- request: AsyncIterable<ExtractInput<T>>,
49
- ) => Promise<OmitConnectrpcFields<ExtractOutput<T>>>
50
- : T extends { methodKind: 'bidi_streaming' }
51
- ? (
52
- request: AsyncIterable<ExtractInput<T>>,
53
- ) => AsyncIterable<OmitConnectrpcFields<ExtractOutput<T>>>
54
- : never;
55
-
56
- /**
57
- * Generic interface that maps a ConnectRPC service to controller methods
58
- *
59
- * Controllers can implement any subset of the service methods.
60
- * TypeScript will enforce correct signatures for implemented methods.
61
- *
62
- * Usage:
63
- * ```typescript
64
- * export class ElizaController implements Service<typeof ElizaService> {
65
- * constructor() {
66
- * ConnectRPC.registerController(this, ElizaService);
67
- * }
68
- *
69
- * async say(request: SayRequest): Promise<SayResponse> {
70
- * // implementation
71
- * }
72
- * // Other methods are optional
73
- * }
74
- * ```
75
- */
76
- export type Service<T> =
77
- T extends GenService<infer Methods>
78
- ? {
79
- [K in keyof Methods]?: ServiceMethod<Methods[K]>;
80
- }
81
- : never;
82
-
83
- export type ServiceMethodNames<T> =
84
- T extends GenService<infer Methods>
85
- ? {
86
- [K in keyof Methods]: K;
87
- }[keyof Methods]
88
- : never;
89
-
90
- /**
91
- * Middleware configuration for ConnectRPC routes - without service specified
92
- */
93
- export type MiddlewareConfigGlobal = {
94
- /**
95
- * The middleware class to apply (must be decorated with @Middleware())
96
- */
97
- use: Type<Middleware>;
98
-
99
- /**
100
- * Middleware applies to all services and all methods
101
- */
102
- on?: never;
103
- methods?: never;
104
- };
105
-
106
- /**
107
- * Middleware configuration for ConnectRPC routes - with service specified
108
- */
109
- export type MiddlewareConfig<T extends GenService<any>> = {
110
- /**
111
- * The middleware class to apply (must be decorated with @Middleware())
112
- */
113
- use: Type<Middleware>;
114
-
115
- /**
116
- * The service to apply middleware to
117
- */
118
- on: T;
119
-
120
- /**
121
- * Optional: Specific method names to apply middleware to.
122
- * If omitted, middleware applies to all methods of the service.
123
- * Method names should match the protobuf method names (e.g., 'say', 'sayMany')
124
- */
125
- methods?: Array<ServiceMethodNames<T>>;
126
- };
127
-
128
- /**
129
- * Middleware configuration for ConnectRPC routes
130
- */
131
- export type MiddlewareConfigUnion =
132
- | MiddlewareConfigGlobal
133
- | MiddlewareConfig<any>;
134
-
135
- /**
136
- * Helper function to create a type-safe middleware configuration
137
- * This ensures proper type inference for method names based on the service
138
- */
139
- export function middlewareConfig<T extends GenService<any>>(
140
- use: Type<Middleware>,
141
- on?: T,
142
- methods?: Array<ServiceMethodNames<T>>,
143
- ): MiddlewareConfigUnion {
144
- return {
145
- use,
146
- on,
147
- methods,
148
- };
149
- }
150
-
151
- export interface ExecutionContext {
152
- getClass<T = any>(): Type<T>;
153
-
154
- getHandler(): Function;
155
-
156
- getArgs<T extends Array<any> = any[]>(): T;
157
-
158
- getArgByIndex<T = any>(index: number): T;
159
-
160
- switchToHttp(): {
161
- getRequest(): FastifyRequest['raw'];
162
- getResponse(): FastifyReply['raw'];
163
- getNext<T = any>(): () => T;
164
- };
165
-
166
- // Adding these two only for compatibility with NestJS ExecutionContext
167
- // Implementations will throw error
168
- switchToRpc(): any;
169
-
170
- switchToWs(): any;
171
- }
172
-
173
- export interface Guard {
174
- canActivate(context: ExecutionContext): boolean | Promise<boolean>;
175
- }