@funduck/connectrpc-fastify 1.0.0 → 1.0.1

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.
@@ -1,71 +0,0 @@
1
- import { FastifyInstance } from 'fastify';
2
- import { convertMiddlewareToHook, logger } from './helpers';
3
- import { MiddlewareConfigUnion } from './interfaces';
4
- import { MiddlewareStore } from './stores';
5
-
6
- export async function initMiddlewares(
7
- server: FastifyInstance,
8
- middlewareConfigs: MiddlewareConfigUnion[],
9
- ) {
10
- for (const config of middlewareConfigs) {
11
- // Convert method names to set with PascalCase
12
- const methods = new Set(
13
- (config.methods || []).map((m) => m[0].toUpperCase() + m.slice(1)),
14
- );
15
-
16
- // Get the middleware instance from the store
17
- const middlewareInstance = MiddlewareStore.getInstance(config.use);
18
-
19
- if (!middlewareInstance) {
20
- logger.warn(
21
- `Middleware ${config.use.name} not found in store. Did you forget to add MiddlewareStore.registerInstance(this) in the constructor? Or did you forget to instantiate the middleware?`,
22
- );
23
- continue;
24
- }
25
-
26
- if (typeof middlewareInstance.use === 'function') {
27
- const hook = convertMiddlewareToHook(middlewareInstance);
28
-
29
- // Create a filtered hook that checks service and method
30
- const filteredHook = async (request: any, reply: any) => {
31
- const url = request.url as string;
32
-
33
- // Parse the URL to get service and method
34
- // Format: /package.ServiceName/MethodName
35
- const match = url.match(/^\/([^/]+)\/([^/]+)$/);
36
-
37
- if (!match) {
38
- // Not a ConnectRPC route, skip
39
- return;
40
- }
41
-
42
- const [, serviceName, methodName] = match;
43
-
44
- // Check if middleware should apply to this service
45
- if (config.on && config.on.typeName !== serviceName) {
46
- return;
47
- }
48
-
49
- // Check if middleware should apply to this method
50
- if (methods.size && !methods.has(methodName)) {
51
- return;
52
- }
53
-
54
- // Apply the middleware
55
- await hook(request, reply);
56
- };
57
-
58
- server.addHook('onRequest', filteredHook);
59
-
60
- const serviceInfo = config.on
61
- ? ` to service ${config.on.typeName}`
62
- : ' to all services';
63
- const methodInfo = config.methods
64
- ? ` methods [${config.methods.join(', ')}]`
65
- : ' all methods';
66
- logger.log(
67
- `Applied middleware: ${config.use.name}${serviceInfo}${methodInfo}`,
68
- );
69
- }
70
- }
71
- }
package/src/stores.ts DELETED
@@ -1,180 +0,0 @@
1
- import { GenService, GenServiceMethods } from '@bufbuild/protobuf/codegenv2';
2
- import { Guard, Middleware, Service, Type } from './interfaces';
3
-
4
- class ControllersStoreClass {
5
- private controllers = new Map<
6
- Type<any>,
7
- {
8
- instance: any;
9
- service: GenService<any>;
10
- }
11
- >();
12
-
13
- values() {
14
- return Array.from(this.controllers.entries()).map(([target, data]) => ({
15
- target,
16
- ...data,
17
- }));
18
- }
19
-
20
- registerInstance<T extends GenServiceMethods>(
21
- self: Service<GenService<T>>,
22
- service: GenService<T>,
23
- {
24
- allowMultipleInstances = false,
25
- }: {
26
- allowMultipleInstances?: boolean;
27
- } = {},
28
- ) {
29
- const controllerClass = self.constructor as Type<any>;
30
- if (!allowMultipleInstances && this.controllers.has(controllerClass)) {
31
- throw new Error(
32
- `Controller ${controllerClass.name} is already registered! This may happen if you export controller as provider and also register it in some Nest module.`,
33
- );
34
- }
35
- this.controllers.set(controllerClass, {
36
- instance: self,
37
- service,
38
- });
39
- }
40
- }
41
-
42
- export const ControllersStore = new ControllersStoreClass();
43
-
44
- /**
45
- * Store for middleware classes and their instances
46
- */
47
- class MiddlewareStoreClass {
48
- private middlewares = new Map<Type<Middleware>, Middleware>();
49
-
50
- /**
51
- * Register a middleware instance from its constructor
52
- */
53
- registerInstance(
54
- self: Middleware,
55
- {
56
- allowMultipleInstances = false,
57
- }: {
58
- allowMultipleInstances?: boolean;
59
- } = {},
60
- ) {
61
- const middlewareClass = self.constructor as Type<Middleware>;
62
- if (!allowMultipleInstances && this.middlewares.has(middlewareClass)) {
63
- throw new Error(
64
- `Middleware ${middlewareClass.name} is already registered! This may happen if you export middleware as provider and also register it in some Nest module.`,
65
- );
66
- }
67
- this.middlewares.set(middlewareClass, self);
68
- }
69
-
70
- /**
71
- * Get a middleware instance by its class
72
- */
73
- getInstance(middlewareClass: Type<Middleware>): Middleware | null {
74
- return this.middlewares.get(middlewareClass) || null;
75
- }
76
- }
77
-
78
- export const MiddlewareStore = new MiddlewareStoreClass();
79
-
80
- /**
81
- * Store for route metadata - maps URL paths to controller class and method info
82
- */
83
- class RouteMetadataStoreClass {
84
- private routes = new Map<
85
- string,
86
- {
87
- controllerClass: Type<any>;
88
- controllerMethod: Function;
89
- controllerMethodName: string;
90
- instance: any;
91
- serviceName: string;
92
- methodName: string;
93
- }
94
- >();
95
-
96
- /**
97
- * Register route metadata for a specific service method
98
- * @param serviceName - The full service name (e.g., "connectrpc.eliza.v1.ElizaService")
99
- * @param methodName - The method name in PascalCase (e.g., "Say")
100
- * @param controllerClass - The controller class
101
- * @param controllerMethod - The bound controller method
102
- * @param controllerMethodName - The name of the controller method
103
- * @param instance - The controller instance
104
- */
105
- registerRoute(
106
- serviceName: string,
107
- methodName: string,
108
- controllerClass: Type<any>,
109
- controllerMethod: Function,
110
- controllerMethodName: string,
111
- instance: any,
112
- ) {
113
- const routeKey = `/${serviceName}/${methodName}`;
114
- this.routes.set(routeKey, {
115
- controllerClass,
116
- controllerMethod,
117
- controllerMethodName,
118
- instance,
119
- serviceName,
120
- methodName,
121
- });
122
- }
123
-
124
- /**
125
- * Get route metadata by URL path
126
- */
127
- getRouteMetadata(urlPath: string) {
128
- return this.routes.get(urlPath) || null;
129
- }
130
-
131
- /**
132
- * Get all registered routes
133
- */
134
- getAllRoutes() {
135
- return Array.from(this.routes.entries());
136
- }
137
- }
138
-
139
- export const RouteMetadataStore = new RouteMetadataStoreClass();
140
-
141
- /**
142
- * Store for guard classes and their instances
143
- */
144
- class GuardsStoreClass {
145
- private guards = new Map<Type<Guard>, Guard>();
146
-
147
- /**
148
- * Register a guard instance from its constructor
149
- */
150
- registerInstance(
151
- self: Guard,
152
- {
153
- allowMultipleInstances = false,
154
- }: { allowMultipleInstances?: boolean } = {},
155
- ) {
156
- const guardClass = self.constructor as Type<Guard>;
157
- if (!allowMultipleInstances && this.guards.has(guardClass)) {
158
- throw new Error(
159
- `Guard ${guardClass.name} is already registered! This may happen if you export guard as provider and also register it in some Nest module.`,
160
- );
161
- }
162
- this.guards.set(guardClass, self);
163
- }
164
-
165
- /**
166
- * Get a guard instance by its class
167
- */
168
- getInstance(guardClass: Type<Guard>): Guard | null {
169
- return this.guards.get(guardClass) || null;
170
- }
171
-
172
- /**
173
- * Get all registered guards
174
- */
175
- getAllGuards(): Guard[] {
176
- return Array.from(this.guards.values());
177
- }
178
- }
179
-
180
- export const GuardsStore = new GuardsStoreClass();
package/src/types.ts DELETED
@@ -1,24 +0,0 @@
1
- import { OptionalKeysOf, Primitive, RequiredKeysOf } from 'type-fest';
2
-
3
- export type OmitFieldsDeep<T, K extends keyof any> = T extends Primitive | Date
4
- ? T
5
- : T extends Array<any>
6
- ? {
7
- [P in keyof T]?: OmitFieldsDeep<T[P], K>;
8
- }
9
- : T extends object
10
- ? {
11
- [P in Exclude<RequiredKeysOf<T>, K>]: OmitFieldsDeep<T[P], K>;
12
- } & {
13
- [P in Exclude<OptionalKeysOf<T>, K>]?: OmitFieldsDeep<T[P], K>;
14
- }
15
- : T;
16
-
17
- /**
18
- * Used to simplify types by omitting ConnectRPC specific fields like `$typeName` and `$unknown`
19
- * The fields are omitted deeply in nested structures.
20
- */
21
- export type OmitConnectrpcFields<T> = OmitFieldsDeep<
22
- T,
23
- '$typeName' | '$unknown'
24
- >;
package/test/buf.gen.yaml DELETED
@@ -1,7 +0,0 @@
1
- version: v2
2
- plugins:
3
- - local: protoc-gen-es
4
- out: gen
5
- # Also generate any imported dependencies
6
- include_imports: true
7
- opt: target=ts
package/test/buf.lock DELETED
@@ -1,6 +0,0 @@
1
- # Generated by buf. DO NOT EDIT.
2
- version: v2
3
- deps:
4
- - name: buf.build/bufbuild/protovalidate
5
- commit: 2a1774d888024a9b93ce7eb4b59f6a83
6
- digest: b5:6b7f9bc919b65e5b79d7b726ffc03d6f815a412d6b792970fa6f065cae162107bd0a9d47272c8ab1a2c9514e87b13d3fbf71df614374d62d2183afb64be2d30a
package/test/buf.yaml DELETED
@@ -1,12 +0,0 @@
1
- # For details on buf.yaml configuration, visit https://buf.build/docs/configuration/v2/buf-yaml
2
- version: v2
3
- lint:
4
- use:
5
- - BASIC
6
- breaking:
7
- use:
8
- - FILE
9
- modules:
10
- - path: proto
11
- deps:
12
- - buf.build/bufbuild/protovalidate
@@ -1,73 +0,0 @@
1
- import { ConnectRPC, OmitConnectrpcFields, Service } from '../src/index';
2
- import type {
3
- SayRequest,
4
- SayResponse,
5
- SayResponses,
6
- } from './gen/connectrpc/eliza/v1/eliza_pb';
7
- import { ElizaService } from './gen/connectrpc/eliza/v1/eliza_pb';
8
-
9
- export class ElizaController implements Service<typeof ElizaService> {
10
- constructor() {
11
- ConnectRPC.registerController(this, ElizaService);
12
- }
13
-
14
- /**
15
- * Unary RPC: Say
16
- * Client sends one request, server sends one response
17
- *
18
- * For demonstration, this method is decorated with @SkipAuthGuard to bypass authentication.
19
- */
20
- async say(
21
- request: SayRequest,
22
-
23
- // You can leave out the return type, it will be inferred from the interface
24
- ) {
25
- console.log(`Controller received request Say`);
26
- return {
27
- sentence: `You said: ${request.sentence}`,
28
- };
29
- }
30
-
31
- /**
32
- * Client Streaming RPC: SayMany
33
- * Client sends multiple requests, server sends one response with all collected
34
- */
35
- async sayMany(
36
- request: AsyncIterable<SayRequest>,
37
-
38
- // You can specify the return type if you want, but you always need to use OmitConnectrpcFields<> because ConnectRPC adds extra fields internally
39
- ): Promise<OmitConnectrpcFields<SayResponses>> {
40
- console.log(`Controller received request SayMany`);
41
-
42
- const responses: OmitConnectrpcFields<SayResponse>[] = [];
43
-
44
- for await (const req of request) {
45
- responses.push({
46
- sentence: `You said: ${req.sentence}`,
47
- });
48
- }
49
-
50
- return {
51
- responses,
52
- };
53
- }
54
-
55
- /**
56
- * Server Streaming RPC: ListenMany
57
- * Client sends one request, server sends multiple responses
58
- */
59
- async *listenMany(request: SayRequest) {
60
- console.log(`Controller received request ListenMany`);
61
-
62
- const words = request.sentence.split(' ');
63
-
64
- for (const word of words) {
65
- // Simulate some processing delay
66
- await new Promise((resolve) => setTimeout(resolve, 100));
67
-
68
- yield {
69
- sentence: `Echo: ${word}`,
70
- };
71
- }
72
- }
73
- }
package/test/e2e-demo.ts DELETED
@@ -1,252 +0,0 @@
1
- import { create } from '@bufbuild/protobuf';
2
- import { createClient } from '@connectrpc/connect';
3
- import { createConnectTransport } from '@connectrpc/connect-node';
4
- import {
5
- ElizaService,
6
- SayRequestSchema,
7
- } from './gen/connectrpc/eliza/v1/eliza_pb';
8
- import { TestGuard1 } from './guards';
9
- import {
10
- TestMiddleware1,
11
- TestMiddleware2,
12
- TestMiddleware3,
13
- } from './middlewares';
14
- import { bootstrap } from './server';
15
-
16
- const transport = createConnectTransport({
17
- baseUrl: 'http://localhost:3000',
18
- httpVersion: '1.1',
19
- });
20
-
21
- export const client = createClient(ElizaService, transport);
22
-
23
- const mockAuthorizationToken = 'Bearer mock-token-123';
24
-
25
- let testMiddlewareCalled = {
26
- 1: false,
27
- 2: false,
28
- 3: false,
29
- };
30
- function prepareMiddlewares() {
31
- testMiddlewareCalled[1] = false;
32
- testMiddlewareCalled[2] = false;
33
- testMiddlewareCalled[3] = false;
34
-
35
- TestMiddleware1.callback = (req, res) => {
36
- console.log(`Middleware 1 called for request: ${req.url}`);
37
- testMiddlewareCalled[1] = true;
38
- return null;
39
- };
40
- TestMiddleware2.callback = (req, res) => {
41
- console.log(`Middleware 2 called for request: ${req.url}`);
42
- testMiddlewareCalled[2] = true;
43
- return null;
44
- };
45
- TestMiddleware3.callback = (req, res) => {
46
- console.log(`Middleware 3 called for request: ${req.url}`);
47
- testMiddlewareCalled[3] = true;
48
- return null;
49
- };
50
- }
51
-
52
- let testGuardCalled = {
53
- 1: false,
54
- };
55
- function prepareGuards() {
56
- testGuardCalled[1] = false;
57
- TestGuard1.callback = (context) => {
58
- console.log(
59
- `Guard 1 called for request:`,
60
- context.switchToHttp().getRequest().url,
61
- );
62
- testGuardCalled[1] = true;
63
- return true;
64
- };
65
- }
66
-
67
- async function testUnary() {
68
- console.log('\n=== Testing Unary RPC: Say ===');
69
- const sentence = 'Hello ConnectRPC!';
70
- console.log(`Request: "${sentence}"`);
71
-
72
- try {
73
- prepareMiddlewares();
74
- prepareGuards();
75
-
76
- const response = await client.say(
77
- { sentence },
78
- {
79
- headers: {
80
- Authorization: mockAuthorizationToken,
81
- 'x-request-id': 'unary-test-001',
82
- },
83
- },
84
- );
85
- console.log(`Response: "${response.sentence}"`);
86
-
87
- // Check that all middlewares were called
88
- if (
89
- !testMiddlewareCalled[1] ||
90
- !testMiddlewareCalled[2] ||
91
- !testMiddlewareCalled[3]
92
- ) {
93
- throw new Error(
94
- `Not all middlewares were called: ${JSON.stringify(
95
- testMiddlewareCalled,
96
- )}`,
97
- );
98
- }
99
-
100
- // Check that the guard was called
101
- if (!testGuardCalled[1]) {
102
- throw new Error('Guard 1 was not called');
103
- }
104
-
105
- console.log('✅ Unary RPC test passed\n');
106
- return true;
107
- } catch (error) {
108
- console.error('❌ Error calling Say:', error);
109
- return false;
110
- }
111
- }
112
-
113
- async function testClientStreaming() {
114
- console.log('=== Testing Client Streaming RPC: SayMany ===');
115
- const sentences = ['First message', 'Second message', 'Third message'];
116
- console.log('Sending multiple requests:', sentences);
117
-
118
- try {
119
- prepareMiddlewares();
120
- prepareGuards();
121
-
122
- // Create an async generator to send multiple requests
123
- async function* generateRequests() {
124
- for (const sentence of sentences) {
125
- console.log(` Sending: "${sentence}"`);
126
- yield create(SayRequestSchema, { sentence });
127
- }
128
- }
129
-
130
- const response = await client.sayMany(generateRequests(), {
131
- headers: {
132
- Authorization: mockAuthorizationToken,
133
- 'x-request-id': 'client-streaming-test-001',
134
- },
135
- });
136
- console.log(`Received ${response.responses.length} responses:`);
137
- response.responses.forEach((resp, idx) => {
138
- console.log(` [${idx + 1}] ${resp.sentence}`);
139
- });
140
-
141
- // Check that all middlewares were called
142
- if (!testMiddlewareCalled[1] || !testMiddlewareCalled[2]) {
143
- throw new Error(
144
- `Not all middlewares were called: ${JSON.stringify(
145
- testMiddlewareCalled,
146
- )}`,
147
- );
148
- }
149
- if (testMiddlewareCalled[3]) {
150
- throw new Error(
151
- `Middleware 3 should not have been called for SayMany: ${JSON.stringify(
152
- testMiddlewareCalled,
153
- )}`,
154
- );
155
- }
156
-
157
- // Check that the guard was called
158
- if (!testGuardCalled[1]) {
159
- throw new Error('Guard 1 was not called');
160
- }
161
-
162
- console.log('✅ Client Streaming RPC test passed\n');
163
- return true;
164
- } catch (error) {
165
- console.error('❌ Error calling SayMany:', error);
166
- return false;
167
- }
168
- }
169
-
170
- async function testServerStreaming() {
171
- console.log('=== Testing Server Streaming RPC: ListenMany ===');
172
- const sentence = 'Hello Streaming World';
173
- console.log(`Request: "${sentence}"`);
174
- console.log('Receiving streamed responses:');
175
-
176
- try {
177
- prepareMiddlewares();
178
- prepareGuards();
179
-
180
- let count = 0;
181
- for await (const response of client.listenMany(
182
- { sentence },
183
- {
184
- headers: {
185
- Authorization: mockAuthorizationToken,
186
- 'x-request-id': 'server-streaming-test-001',
187
- },
188
- },
189
- )) {
190
- count++;
191
- console.log(` [${count}] ${response.sentence}`);
192
- }
193
-
194
- // Check that all middlewares were called
195
- if (!testMiddlewareCalled[1] || !testMiddlewareCalled[2]) {
196
- throw new Error(
197
- `Not all middlewares were called: ${JSON.stringify(
198
- testMiddlewareCalled,
199
- )}`,
200
- );
201
- }
202
- if (testMiddlewareCalled[3]) {
203
- throw new Error(
204
- `Middleware 3 should not have been called for SayMany: ${JSON.stringify(
205
- testMiddlewareCalled,
206
- )}`,
207
- );
208
- }
209
- // Check that the guard was called
210
- if (!testGuardCalled[1]) {
211
- throw new Error('Guard 1 was not called');
212
- }
213
-
214
- console.log(
215
- `✅ Server Streaming RPC test passed (received ${count} responses)\n`,
216
- );
217
- return true;
218
- } catch (error) {
219
- console.error('❌ Error calling ListenMany:', error);
220
- return false;
221
- }
222
- }
223
-
224
- async function runAllTests() {
225
- console.log('🚀 Starting ConnectRPC Tests\n');
226
-
227
- // Bootstrap the server
228
- await bootstrap();
229
-
230
- // Give the server a moment to start
231
- await new Promise((resolve) => setTimeout(resolve, 500));
232
-
233
- // Run all tests
234
- const results = [
235
- await testUnary(),
236
- await testClientStreaming(),
237
- await testServerStreaming(),
238
- ];
239
-
240
- // Check results
241
- const allPassed = results.every((result) => result === true);
242
-
243
- if (allPassed) {
244
- console.log('🎉 All tests passed!');
245
- process.exit(0);
246
- } else {
247
- console.log('❌ Some tests failed');
248
- process.exit(1);
249
- }
250
- }
251
-
252
- runAllTests();