Skip to content

Commit f3b6057

Browse files
authored
Merge pull request #3325 from SeedCompany/no-middleware
2 parents 2f094d4 + b7e58d6 commit f3b6057

File tree

10 files changed

+89
-49
lines changed

10 files changed

+89
-49
lines changed

.eslintrc.cjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,9 @@ const restrictedImports = [
7979
replacement: { path: '~/core/http' },
8080
},
8181
{
82-
importNames: 'NestMiddleware',
82+
importNames: ['NestMiddleware', 'NestModule'],
8383
path: '@nestjs/common',
84-
replacement: { importName: 'HttpMiddleware', path: '~/core/http' },
84+
message: 'Do not use express/connect middleware',
8585
},
8686
{
8787
importNames: ['RouteConfig', 'RouteConstraints'],
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { DiscoveredMethodWithMeta } from '@golevelup/nestjs-discovery';
2+
3+
export const uniqueDiscoveredMethods = <T>(
4+
methods: Array<DiscoveredMethodWithMeta<T>>,
5+
) => {
6+
const seenClasses = new Map<object, Map<string, Set<unknown>>>();
7+
const uniqueMethods = [] as typeof methods;
8+
for (const method of methods) {
9+
const clsInstance = method.discoveredMethod.parentClass.instance;
10+
const methodName = method.discoveredMethod.methodName;
11+
if (!seenClasses.has(clsInstance)) {
12+
seenClasses.set(clsInstance, new Map());
13+
}
14+
const seenMethods = seenClasses.get(clsInstance)!;
15+
if (!seenMethods.has(methodName)) {
16+
seenMethods.set(methodName, new Set());
17+
}
18+
const seenMetadata = seenMethods.get(methodName)!;
19+
if (!seenMetadata.has(method.meta)) {
20+
seenMetadata.add(method.meta);
21+
uniqueMethods.push(method);
22+
}
23+
}
24+
return uniqueMethods;
25+
};

src/components/authentication/authentication.module.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
1-
import {
2-
forwardRef,
3-
Global,
4-
MiddlewareConsumer,
5-
Module,
6-
NestModule,
7-
} from '@nestjs/common';
1+
import { forwardRef, Global, Module } from '@nestjs/common';
82
import { APP_INTERCEPTOR } from '@nestjs/core';
93
import { splitDb } from '~/core';
104
import { AuthorizationModule } from '../authorization/authorization.module';
@@ -54,12 +48,4 @@ import { SessionResolver } from './session.resolver';
5448
AuthenticationRepository,
5549
],
5650
})
57-
export class AuthenticationModule implements NestModule {
58-
constructor(
59-
private readonly currentUserProvider: EdgeDBCurrentUserProvider,
60-
) {}
61-
62-
configure(consumer: MiddlewareConsumer) {
63-
consumer.apply(this.currentUserProvider.use).forRoutes('*');
64-
}
65-
}
51+
export class AuthenticationModule {}

src/components/authentication/current-user.provider.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,30 @@ import { isUUID } from 'class-validator';
99
import { BehaviorSubject, identity } from 'rxjs';
1010
import { Session } from '~/common';
1111
import { EdgeDB, OptionsFn } from '~/core/edgedb';
12-
import { HttpMiddleware } from '~/core/http';
12+
import { GlobalHttpHook } from '~/core/http';
1313

1414
@Injectable()
15-
export class EdgeDBCurrentUserProvider
16-
implements HttpMiddleware, NestInterceptor
17-
{
15+
export class EdgeDBCurrentUserProvider implements NestInterceptor {
1816
// A map to transfer the options' holder
1917
// between the creation in middleware and the use in the interceptor.
2018
private readonly optionsHolderByRequest = new WeakMap<
21-
Parameters<HttpMiddleware['use']>[0],
19+
Parameters<GlobalHttpHook>[0]['raw'],
2220
BehaviorSubject<OptionsFn>
2321
>();
2422

2523
constructor(private readonly edgedb: EdgeDB) {}
2624

27-
use: HttpMiddleware['use'] = (req, res, next) => {
25+
@GlobalHttpHook()
26+
onRequest(...[req, _reply, next]: Parameters<GlobalHttpHook>) {
2827
// Create holder to use later to add current user to globals after it is fetched
2928
const optionsHolder = new BehaviorSubject<OptionsFn>(identity);
30-
this.optionsHolderByRequest.set(req, optionsHolder);
29+
this.optionsHolderByRequest.set(req.raw, optionsHolder);
3130

3231
// These options should apply to the entire HTTP/GQL operation.
3332
// Connect middleware is the only place we get a function which has all of
3433
// this in scope for the use of an ALS context.
3534
this.edgedb.usingOptions(optionsHolder, next);
36-
};
35+
}
3736

3837
/**
3938
* Connect the session to the options' holder

src/core/graphql/driver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export class Driver extends YogaDriver<'fastify'> {
2020
);
2121
options.plugins = [
2222
...(options.plugins ?? []),
23-
...discoveredPlugins.map((cls) => cls.discoveredClass.instance),
23+
...new Set(discoveredPlugins.map((cls) => cls.discoveredClass.instance)),
2424
];
2525

2626
await super.start(options);

src/core/http/decorators.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Many } from '@seedcompany/common';
66
import { createMetadataDecorator } from '@seedcompany/nest';
77
import { FastifyContextConfig } from 'fastify';
88
import type { RouteConstraint } from 'fastify/types/route';
9+
import { HttpHooks } from './types';
910

1011
export const RouteConstraints = createMetadataDecorator({
1112
key: FASTIFY_ROUTE_CONSTRAINTS_METADATA,
@@ -54,3 +55,10 @@ export const RawBody = createMetadataDecorator({
5455
} = {},
5556
) => config,
5657
});
58+
59+
export const GlobalHttpHook = createMetadataDecorator({
60+
types: ['method'],
61+
setter: (hook: keyof HttpHooks = 'preHandler') => hook,
62+
});
63+
export type GlobalHttpHook<Name extends keyof HttpHooks = 'preHandler'> =
64+
HttpHooks[Name];

src/core/http/http.adapter.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import compression from '@fastify/compress';
22
import cookieParser from '@fastify/cookie';
33
import cors from '@fastify/cors';
4+
import { DiscoveryService } from '@golevelup/nestjs-discovery';
45
import {
56
VERSION_NEUTRAL,
67
type VersionValue,
@@ -14,9 +15,15 @@ import {
1415
import type { FastifyInstance, HTTPMethods, RouteOptions } from 'fastify';
1516
import rawBody from 'fastify-raw-body';
1617
import * as zlib from 'node:zlib';
18+
import { uniqueDiscoveredMethods } from '~/common/discovery-unique-methods';
1719
import { ConfigService } from '~/core/config/config.service';
18-
import { RawBody, RouteConfig, RouteConstraints } from './decorators';
19-
import type { CookieOptions, CorsOptions, IResponse } from './types';
20+
import {
21+
GlobalHttpHook,
22+
RawBody,
23+
RouteConfig,
24+
RouteConstraints,
25+
} from './decorators';
26+
import type { CookieOptions, CorsOptions, HttpHooks, IResponse } from './types';
2027

2128
export type NestHttpApplication = NestFastifyApplication & {
2229
configure: (
@@ -61,6 +68,18 @@ export class HttpAdapter extends PatchedFastifyAdapter {
6168
app.setGlobalPrefix(config.hostUrl$.value.pathname.slice(1));
6269

6370
config.applyTimeouts(app.getHttpServer(), config.httpTimeouts);
71+
72+
// Attach hooks
73+
const globalHooks = await app
74+
.get(DiscoveryService)
75+
.providerMethodsWithMetaAtKey<keyof HttpHooks>(GlobalHttpHook.KEY);
76+
const fastify = app.getHttpAdapter().getInstance();
77+
for (const globalHook of uniqueDiscoveredMethods(globalHooks)) {
78+
const handler = globalHook.discoveredMethod.handler.bind(
79+
globalHook.discoveredMethod.parentClass.instance,
80+
);
81+
fastify.addHook(globalHook.meta, handler);
82+
}
6483
}
6584

6685
protected injectRouteOptions(
@@ -117,6 +136,13 @@ export class HttpAdapter extends PatchedFastifyAdapter {
117136
return this.instance.route(route);
118137
}
119138

139+
override registerMiddie() {
140+
// no
141+
}
142+
override createMiddlewareFactory(): never {
143+
throw new Error('Express/Connect Middleware should not be used');
144+
}
145+
120146
setCookie(
121147
response: IResponse,
122148
name: string,

src/core/http/types.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
/* eslint-disable @typescript-eslint/method-signature-style */
2-
// eslint-disable-next-line @seedcompany/no-restricted-imports
3-
import type { NestMiddleware } from '@nestjs/common';
42
import type {
53
FastifyRequest as Request,
64
FastifyReply as Response,
5+
RouteShorthandOptions,
76
} from 'fastify';
87
import type { Session } from '~/common';
98

109
// Exporting with I prefix to avoid ambiguity with web global types
1110
export type { Request as IRequest, Response as IResponse };
1211

13-
export type HttpMiddleware = NestMiddleware<Request['raw'], Response['raw']>;
12+
export type HttpHooks = Required<{
13+
[Hook in keyof RouteShorthandOptions as Exclude<
14+
Extract<Hook, `${'pre' | 'on'}${string}`>,
15+
`${'prefix'}${string}`
16+
>]: Exclude<RouteShorthandOptions[Hook], any[]>;
17+
}>;
1418

1519
export { FastifyCorsOptions as CorsOptions } from '@fastify/cors';
1620
export { SerializeOptions as CookieOptions } from '@fastify/cookie';

src/core/tracing/tracing.module.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
import {
2-
MiddlewareConsumer,
3-
Module,
4-
NestModule,
5-
OnModuleInit,
6-
} from '@nestjs/common';
1+
import { Module, OnModuleInit } from '@nestjs/common';
72
import { APP_INTERCEPTOR } from '@nestjs/core';
83
import XRay from 'aws-xray-sdk-core';
94
import { ConfigService } from '../config/config.service';
@@ -24,17 +19,13 @@ import { XRayMiddleware } from './xray.middleware';
2419
],
2520
exports: [TracingService],
2621
})
27-
export class TracingModule implements OnModuleInit, NestModule {
22+
export class TracingModule implements OnModuleInit {
2823
constructor(
2924
@Logger('xray') private readonly logger: ILogger,
3025
private readonly config: ConfigService,
3126
private readonly version: VersionService,
3227
) {}
3328

34-
configure(consumer: MiddlewareConsumer) {
35-
consumer.apply(XRayMiddleware).forRoutes('*');
36-
}
37-
3829
async onModuleInit() {
3930
// Don't use cls-hooked lib. It's old and Node has AsyncLocalStorage now.
4031
XRay.enableManualMode();

src/core/tracing/xray.middleware.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import {
66
} from '@nestjs/common';
77
import { GqlExecutionContext } from '@nestjs/graphql';
88
import XRay from 'aws-xray-sdk-core';
9-
import { HttpAdapter, HttpMiddleware as NestMiddleware } from '~/core/http';
9+
import { GlobalHttpHook, HttpAdapter } from '~/core/http';
1010
import { ConfigService } from '../config/config.service';
1111
import { Sampler } from './sampler';
1212
import { TracingService } from './tracing.service';
1313

1414
@Injectable()
15-
export class XRayMiddleware implements NestMiddleware, NestInterceptor {
15+
export class XRayMiddleware implements NestInterceptor {
1616
constructor(
1717
private readonly tracing: TracingService,
1818
private readonly sampler: Sampler,
@@ -23,12 +23,13 @@ export class XRayMiddleware implements NestMiddleware, NestInterceptor {
2323
/**
2424
* Setup root segment for request/response.
2525
*/
26-
use: NestMiddleware['use'] = (req, res, next) => {
26+
@GlobalHttpHook()
27+
onRequest(...[req, res, next]: Parameters<GlobalHttpHook>) {
2728
const traceData = XRay.utils.processTraceData(
2829
req.headers['x-amzn-trace-id'] as string | undefined,
2930
);
3031
const root = new XRay.Segment('cord', traceData.root, traceData.parent);
31-
const reqData = new XRay.middleware.IncomingRequestData(req);
32+
const reqData = new XRay.middleware.IncomingRequestData(req.raw);
3233
root.addIncomingRequestData(reqData);
3334
// Use public DNS as url instead of specific IP
3435
// @ts-expect-error xray library types suck
@@ -40,7 +41,7 @@ export class XRayMiddleware implements NestMiddleware, NestInterceptor {
4041
enumerable: false,
4142
});
4243

43-
res.on('finish', () => {
44+
res.raw.on('finish', () => {
4445
const status = res.statusCode.toString();
4546
if (status.startsWith('4')) {
4647
root.addErrorFlag();
@@ -57,7 +58,7 @@ export class XRayMiddleware implements NestMiddleware, NestInterceptor {
5758
});
5859

5960
this.tracing.segmentStorage.run(root, next);
60-
};
61+
}
6162

6263
/**
6364
* Determine if segment should be traced/sampled.

0 commit comments

Comments
 (0)