Skip to content

Commit

Permalink
Merge pull request #3298 from SeedCompany/http-adapter
Browse files Browse the repository at this point in the history
More HttpAdapter usage
  • Loading branch information
CarsonF authored Sep 30, 2024
2 parents f1f76d9 + 368a681 commit b3fa3ea
Show file tree
Hide file tree
Showing 9 changed files with 92 additions and 35 deletions.
4 changes: 3 additions & 1 deletion src/components/authentication/session.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
UnauthenticatedException,
} from '~/common';
import { ConfigService, ILogger, Loader, LoaderOf, Logger } from '~/core';
import { HttpAdapter } from '~/core/http';
import { Privileges } from '../authorization';
import { Power } from '../authorization/dto';
import { UserLoader, UserService } from '../user';
Expand All @@ -29,6 +30,7 @@ export class SessionResolver {
private readonly config: ConfigService,
private readonly sessionInt: SessionInterceptor,
private readonly users: UserService,
private readonly http: HttpAdapter,
@Logger('session:resolver') private readonly logger: ILogger,
) {}

Expand Down Expand Up @@ -76,7 +78,7 @@ export class SessionResolver {
'Cannot use cookie session without a response object',
);
}
context.response.cookie(name, token, {
this.http.setCookie(context.response, name, token, {
...options,
expires: expires
? DateTime.local().plus(expires).toJSDate()
Expand Down
9 changes: 4 additions & 5 deletions src/components/file/file-url.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from '@nestjs/common';
import { ID } from '~/common';
import { loggedInSession as verifyLoggedIn } from '~/common/session';
import { HttpAdapterHost, IRequest, IResponse } from '~/core/http';
import { HttpAdapter, IRequest, IResponse } from '~/core/http';
import { SessionInterceptor } from '../authentication/session.interceptor';
import { FileService } from './file.service';

Expand All @@ -23,7 +23,7 @@ export class FileUrlController {
@Inject(forwardRef(() => FileService))
private readonly files: FileService & {},
private readonly sessionHost: SessionInterceptor,
private readonly httpAdapterHost: HttpAdapterHost,
private readonly http: HttpAdapter,
) {}

@Get(':fileId/:fileName?')
Expand All @@ -45,8 +45,7 @@ export class FileUrlController {
const url = await this.files.getDownloadUrl(node, download != null);
const cacheControl = this.files.determineCacheHeader(node);

const { httpAdapter } = this.httpAdapterHost;
httpAdapter.setHeader(res, 'Cache-Control', cacheControl);
httpAdapter.redirect(res, HttpStatus.FOUND, url);
this.http.setHeader(res, 'Cache-Control', cacheControl);
this.http.redirect(res, HttpStatus.FOUND, url);
}
}
11 changes: 7 additions & 4 deletions src/components/file/local-bucket.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { DateTime } from 'luxon';
import { URL } from 'node:url';
import rawBody from 'raw-body';
import { InputException } from '~/common';
import { IRequest, IResponse } from '~/core/http';
import { HttpAdapter, IRequest, IResponse } from '~/core/http';
import { FileBucket, InvalidSignedUrlException } from './bucket';

/**
Expand All @@ -21,7 +21,10 @@ import { FileBucket, InvalidSignedUrlException } from './bucket';
export class LocalBucketController {
static path = '/local-bucket';

constructor(private readonly bucket: FileBucket) {}
constructor(
private readonly bucket: FileBucket,
private readonly http: HttpAdapter,
) {}

@Put()
async upload(
Expand Down Expand Up @@ -72,7 +75,7 @@ export class LocalBucketController {
'Content-Disposition': out.ContentDisposition,
'Content-Encoding': out.ContentEncoding,
'Content-Language': out.ContentLanguage,
'Content-Length': out.ContentLength,
'Content-Length': String(out.ContentLength),
'Content-Type': out.ContentType,
Expires: out.Expires
? DateTime.fromJSDate(out.Expires).toHTTP()
Expand All @@ -84,7 +87,7 @@ export class LocalBucketController {
};
for (const [header, val] of Object.entries(headers)) {
if (val != null) {
res.setHeader(header, val);
this.http.setHeader(res, header, val);
}
}

Expand Down
7 changes: 3 additions & 4 deletions src/core/exception/exception.filter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ArgumentsHost, Catch, HttpStatus, Injectable } from '@nestjs/common';
import { GqlExceptionFilter } from '@nestjs/graphql';
import { mapValues } from '@seedcompany/common';
import { HttpAdapterHost } from '~/core/http';
import { HttpAdapter } from '~/core/http';
import { ConfigService } from '../config/config.service';
import { ILogger, Logger, LogLevel } from '../logger';
import { ValidationException } from '../validation';
Expand All @@ -12,7 +12,7 @@ import { isFromHackAttempt } from './is-from-hack-attempt';
@Injectable()
export class ExceptionFilter implements GqlExceptionFilter {
constructor(
private readonly httpAdapterHost: HttpAdapterHost,
private readonly http: HttpAdapter,
@Logger('nest') private readonly logger: ILogger,
private readonly config: ConfigService,
private readonly normalizer: ExceptionNormalizer,
Expand Down Expand Up @@ -77,9 +77,8 @@ export class ExceptionFilter implements GqlExceptionFilter {
: ex.stack.split('\n'),
};

const { httpAdapter } = this.httpAdapterHost;
const res = args.switchToHttp().getResponse();
httpAdapter.reply(res, out, status);
this.http.reply(res, out, status);
}

logIt(info: ExceptionJson, error: Error) {
Expand Down
32 changes: 29 additions & 3 deletions src/core/http/http.adapter.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,36 @@
// eslint-disable-next-line @seedcompany/no-restricted-imports
import { HttpAdapterHost as HttpAdapterHostImpl } from '@nestjs/core';
import {
ExpressAdapter as HttpAdapter,
NestExpressApplication as NestHttpApplication,
NestExpressApplication as BaseApplication,
ExpressAdapter,
} from '@nestjs/platform-express';
import cookieParser from 'cookie-parser';
import { ConfigService } from '../config/config.service';
import type { CorsOptions } from './index';
import { CookieOptions, IResponse } from './types';

export { HttpAdapter, type NestHttpApplication };
export type NestHttpApplication = BaseApplication & {
configure: (app: BaseApplication, config: ConfigService) => Promise<void>;
};

export class HttpAdapterHost extends HttpAdapterHostImpl<HttpAdapter> {}

export class HttpAdapter extends ExpressAdapter {
async configure(app: BaseApplication, config: ConfigService) {
app.enableCors(config.cors as CorsOptions); // typecast to undo deep readonly
app.use(cookieParser());

app.setGlobalPrefix(config.hostUrl$.value.pathname.slice(1));

config.applyTimeouts(app.getHttpServer(), config.httpTimeouts);
}

setCookie(
response: IResponse,
name: string,
value: string,
options: CookieOptions,
) {
response.cookie(name, value, options);
}
}
36 changes: 34 additions & 2 deletions src/core/http/http.module.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,47 @@
import { Module } from '@nestjs/common';
// eslint-disable-next-line @seedcompany/no-restricted-imports
import { HttpAdapterHost as HttpAdapterHostImpl } from '@nestjs/core';
import { HttpAdapterHost } from './http.adapter';
import { setOf } from '@seedcompany/common';
import { getParentTypes } from '~/common';
import { HttpAdapter, HttpAdapterHost } from './http.adapter';

@Module({
providers: [
{
provide: HttpAdapterHost,
useExisting: HttpAdapterHostImpl,
},
{
provide: HttpAdapter,
inject: [HttpAdapterHost],
useFactory: async (host: HttpAdapterHost) => {
const availableKeys = setOf(
getParentTypes(HttpAdapter).flatMap((cls) => [
...Object.getOwnPropertyNames(cls.prototype),
...Object.getOwnPropertySymbols(cls.prototype),
]),
);
return new Proxy(host, {
get(_, key, receiver) {
const { httpAdapter } = host;
if (key === 'httpAdapter') {
return httpAdapter;
}
if (key === 'constructor') {
return HttpAdapter.constructor;
}
if (!availableKeys.has(key)) {
return undefined;
}
if (!httpAdapter) {
throw new Error('HttpAdapter is not yet available');
}
return Reflect.get(httpAdapter, key, receiver);
},
});
},
},
],
exports: [HttpAdapterHost],
exports: [HttpAdapter, HttpAdapterHost],
})
export class HttpModule {}
11 changes: 6 additions & 5 deletions src/core/tracing/xray.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import XRay from 'aws-xray-sdk-core';
import { HttpMiddleware as NestMiddleware } from '~/core/http';
import { HttpAdapter, HttpMiddleware as NestMiddleware } from '~/core/http';
import { ConfigService } from '../config/config.service';
import { Sampler } from './sampler';
import { TracingService } from './tracing.service';
Expand All @@ -17,22 +17,22 @@ export class XRayMiddleware implements NestMiddleware, NestInterceptor {
private readonly tracing: TracingService,
private readonly sampler: Sampler,
private readonly config: ConfigService,
private readonly http: HttpAdapter,
) {}

/**
* Setup root segment for request/response.
*/
use: NestMiddleware['use'] = (req, res, next) => {
const traceData = XRay.utils.processTraceData(
req.header('x-amzn-trace-id'),
req.headers['x-amzn-trace-id'] as string | undefined,
);
const root = new XRay.Segment('cord', traceData.root, traceData.parent);
const reqData = new XRay.middleware.IncomingRequestData(req);
root.addIncomingRequestData(reqData);
// Use public DNS as url instead of specific IP
// @ts-expect-error xray library types suck
root.http.request.url =
this.config.hostUrl$.value + req.originalUrl.slice(1);
root.http.request.url = this.config.hostUrl$.value + req.url.slice(1);

// Add to segment so interceptor can access without having to calculate again.
Object.defineProperty(reqData, 'traceData', {
Expand Down Expand Up @@ -102,7 +102,8 @@ export class XRayMiddleware implements NestMiddleware, NestInterceptor {
: context.switchToHttp().getResponse();

if (res && root instanceof XRay.Segment) {
res.setHeader(
this.http.setHeader(
res,
'x-amzn-trace-id',
`Root=${root.trace_id};Sampled=${sampled ? '1' : '0'}`,
);
Expand Down
10 changes: 2 additions & 8 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import cookieParser from 'cookie-parser';
import type { CorsOptions, NestHttpApplication } from '~/core/http';
import type { NestHttpApplication } from '~/core/http';
import './polyfills';

async function bootstrap() {
Expand Down Expand Up @@ -31,12 +30,7 @@ async function bootstrap() {
);
const config = app.get(ConfigService);

app.enableCors(config.cors as CorsOptions); // typecast to undo deep readonly
app.use(cookieParser());

app.setGlobalPrefix(config.hostUrl$.value.pathname.slice(1));

config.applyTimeouts(app.getHttpServer(), config.httpTimeouts);
await app.configure(app, config);

app.enableShutdownHooks();
await app.listen(config.port, () => {
Expand Down
7 changes: 4 additions & 3 deletions test/utility/create-app.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { faker } from '@faker-js/faker';
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { andCall } from '~/common';
import { HttpAdapter } from '~/core/http';
import { ConfigService } from '~/core';
import { HttpAdapter, NestHttpApplication } from '~/core/http';
import { LogLevel } from '~/core/logger';
import { LevelMatcher } from '~/core/logger/level-matcher';
import { AppModule } from '../../src/app.module';
Expand All @@ -17,7 +17,7 @@ const origEmail = faker.internet.email.bind(faker.internet);
faker.internet.email = (...args) =>
origEmail(...(args as any)).replace('@', `.${Date.now()}@`);

export interface TestApp extends INestApplication {
export interface TestApp extends NestHttpApplication {
graphql: GraphQLTestClient;
}

Expand All @@ -35,6 +35,7 @@ export const createTestApp = async () => {
.compile();

const app = moduleFixture.createNestApplication<TestApp>(new HttpAdapter());
await app.configure(app, app.get(ConfigService));
await app.init();
app.graphql = await createGraphqlClient(app);

Expand Down

0 comments on commit b3fa3ea

Please sign in to comment.