Skip to content

Commit

Permalink
Merge pull request #3292 from SeedCompany/fastify
Browse files Browse the repository at this point in the history
  • Loading branch information
CarsonF authored Oct 18, 2024
2 parents 8ae4410 + 531d326 commit 6980e8f
Show file tree
Hide file tree
Showing 13 changed files with 910 additions and 536 deletions.
10 changes: 5 additions & 5 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,6 @@ const oldRestrictedImports = [
importNames: ['Dictionary', 'SafeDictionary'],
message: 'Use a type with strict keys instead',
},
{
name: 'express-serve-static-core',
importNames: ['Dictionary'],
message: 'Use a type with strict keys instead',
},
];

/** @type {import('@seedcompany/eslint-plugin').ImportRestriction[]} */
Expand Down Expand Up @@ -88,6 +83,11 @@ const restrictedImports = [
path: '@nestjs/common',
replacement: { importName: 'HttpMiddleware', path: '~/core/http' },
},
{
importNames: ['RouteConfig', 'RouteConstraints'],
path: '@nestjs/platform-fastify',
replacement: { path: '~/core/http' },
},
];

const namingConvention = [
Expand Down
17 changes: 11 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,21 @@
"dependencies": {
"@apollo/server": "^4.9.5",
"@apollo/subgraph": "^2.5.6",
"@as-integrations/fastify": "^2.1.1",
"@aws-sdk/client-s3": "^3.440.0",
"@aws-sdk/s3-request-presigner": "^3.440.0",
"@faker-js/faker": "^8.2.0",
"@fastify/compress": "^7.0.3",
"@fastify/cookie": "^9.4.0",
"@fastify/cors": "^9.0.1",
"@ffprobe-installer/ffprobe": "^2.1.2",
"@golevelup/nestjs-discovery": "^4.0.0",
"@leeoniya/ufuzzy": "^1.0.11",
"@nestjs/apollo": "^12.0.9",
"@nestjs/common": "^10.2.7",
"@nestjs/core": "^10.2.7",
"@nestjs/graphql": "^12.0.9",
"@nestjs/platform-express": "^10.2.7",
"@nestjs/platform-fastify": "^10.4.3",
"@patarapolw/prettyprint": "^1.0.3",
"@seedcompany/cache": "^2.0.0",
"@seedcompany/common": ">=0.13.1 <1",
Expand All @@ -59,16 +63,16 @@
"cli-table3": "^0.6.3",
"clipanion": "^4.0.0-rc.3",
"common-tags": "^1.8.2",
"cookie-parser": "^1.4.6",
"cypher-query-builder": "patch:cypher-query-builder@npm%3A6.0.4#~/.yarn/patches/cypher-query-builder-npm-6.0.4-e8707a5e8e.patch",
"dotenv": "^16.3.1",
"dotenv-expand": "^10.0.0",
"edgedb": "^1.6.0-canary.20240827T111834",
"execa": "^8.0.1",
"express": "^4.18.2",
"extensionless": "^1.7.0",
"fast-safe-stringify": "^2.1.1",
"fastest-levenshtein": "^1.0.16",
"fastify": "^4.28.1",
"fastify-raw-body": "^4.3.0",
"file-type": "^18.6.0",
"glob": "^10.3.10",
"got": "^14.3.0",
Expand Down Expand Up @@ -116,9 +120,6 @@
"@seedcompany/eslint-plugin": "^3.4.1",
"@tsconfig/strictest": "^2.0.2",
"@types/common-tags": "^1.8.3",
"@types/cookie-parser": "^1.4.5",
"@types/express": "^4.17.20",
"@types/express-serve-static-core": "^4.17.39",
"@types/ffprobe": "^1.1.7",
"@types/graphql-upload": "^16.0.4",
"@types/jest": "^29.5.7",
Expand Down Expand Up @@ -154,9 +155,13 @@
"neo4j-driver-bolt-connection@npm:5.20.0": "patch:neo4j-driver-bolt-connection@npm%3A5.20.0#~/.yarn/patches/neo4j-driver-bolt-connection-npm-5.20.0-1f7809f435.patch",
"neo4j-driver-core@npm:5.20.0": "patch:neo4j-driver-core@npm%3A5.20.0#~/.yarn/patches/neo4j-driver-core-npm-5.20.0-99216f6938.patch",
"@apollo/server-plugin-landing-page-graphql-playground": "npm:empty-npm-package@*",
"@apollo/server/express": "npm:empty-npm-package@*",
"@nestjs/cli/fork-ts-checker-webpack-plugin": "npm:empty-npm-package@*",
"@nestjs/cli/webpack": "npm:empty-npm-package@*",
"@nestjs/cli/typescript": "^5.1.6",
"@types/express": "npm:@types/stack-trace@*",
"@types/express-serve-static-core": "npm:@types/stack-trace@*",
"@types/koa": "npm:@types/stack-trace@*",
"subscriptions-transport-ws": "npm:empty-npm-package@*"
},
"dependenciesMeta": {
Expand Down
4 changes: 2 additions & 2 deletions src/components/authentication/current-user.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ export class EdgeDBCurrentUserProvider
const { request, session$ } =
GqlExecutionContext.create(context).getContext();
if (request) {
const optionsHolder = this.optionsHolderByRequest.get(request)!;
const optionsHolder = this.optionsHolderByRequest.get(request.raw)!;
session$.subscribe((session) => {
this.applyToOptions(session, optionsHolder);
});
}
} else if (type === 'http') {
const request = context.switchToHttp().getRequest();
const optionsHolder = this.optionsHolderByRequest.get(request)!;
const optionsHolder = this.optionsHolderByRequest.get(request.raw)!;
this.applyToOptions(request.session, optionsHolder);
}

Expand Down
11 changes: 5 additions & 6 deletions src/components/file/local-bucket.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
Body,
Controller,
Get,
Headers,
Expand All @@ -9,9 +10,8 @@ import {
} from '@nestjs/common';
import { DateTime } from 'luxon';
import { URL } from 'node:url';
import rawBody from 'raw-body';
import { InputException } from '~/common';
import { HttpAdapter, IRequest, IResponse } from '~/core/http';
import { HttpAdapter, IRequest, IResponse, RawBody } from '~/core/http';
import { FileBucket, InvalidSignedUrlException } from './bucket';

/**
Expand All @@ -27,14 +27,13 @@ export class LocalBucketController {
) {}

@Put()
@RawBody({ passthrough: true })
async upload(
@Headers('content-type') contentType: string,
@Request() req: IRequest,
@Body() contents: Buffer,
) {
// Chokes on json files because they are parsed with body-parser.
// Need to disable it for this path or create a workaround.
const contents = await rawBody(req);
if (!contents) {
if (!contents || !Buffer.isBuffer(contents)) {
throw new InputException();
}

Expand Down
2 changes: 1 addition & 1 deletion src/core/exception/exception.filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class ExceptionFilter implements GqlExceptionFilter {

const hack = isFromHackAttempt(exception, args);
if (hack) {
hack.destroy();
hack.raw.destroy();
return;
}

Expand Down
35 changes: 30 additions & 5 deletions src/core/graphql/graphql.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { ApolloDriver } from '@nestjs/apollo';
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { GraphQLModule as NestGraphqlModule } from '@nestjs/graphql';
import createUploadMiddleware from 'graphql-upload/graphqlUploadExpress.mjs';
import processUploadRequest, {
UploadOptions,
} from 'graphql-upload/processRequest.mjs';
import { HttpAdapterHost } from '~/core/http';
import { TracingModule } from '../tracing';
import { GqlContextHost, GqlContextHostImpl } from './gql-context.host';
import { GraphqlErrorFormatter } from './graphql-error-formatter';
Expand All @@ -13,6 +16,8 @@ import { GraphqlOptions } from './graphql.options';

import './types';

const FileUploadOptions: UploadOptions = {};

@Module({
imports: [TracingModule],
providers: [
Expand Down Expand Up @@ -42,15 +47,35 @@ export class GraphqlOptionsModule {}
exports: [NestGraphqlModule, GqlContextHost],
})
export class GraphqlModule implements NestModule {
constructor(private readonly middleware: GqlContextHostImpl) {}
constructor(
private readonly middleware: GqlContextHostImpl,
private readonly app: HttpAdapterHost,
) {}

configure(consumer: MiddlewareConsumer) {
// Always attach our GQL Context middleware.
// It has its own logic to handle non-gql requests.
consumer.apply(this.middleware.use).forRoutes('*');

// Attach the graphql-upload middleware to the graphql endpoint.
const uploadMiddleware = createUploadMiddleware();
consumer.apply(uploadMiddleware).forRoutes('/graphql', '/graphql/*');
// Setup file upload handling
const fastify = this.app.httpAdapter.getInstance();
const multipartRequests = new WeakSet();
fastify.addContentTypeParser(
'multipart/form-data',
(req, payload, done) => {
multipartRequests.add(req);
done(null);
},
);
fastify.addHook('preValidation', async (req, reply) => {
if (!multipartRequests.has(req) || !req.url.startsWith('/graphql')) {
return;
}
req.body = await processUploadRequest(
req.raw,
reply.raw,
FileUploadOptions,
);
});
}
}
25 changes: 13 additions & 12 deletions src/core/graphql/graphql.options.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { ContextFunction } from '@apollo/server';
import { ExpressContextFunctionArgument } from '@apollo/server/express4';
import {
ApolloServerPluginLandingPageLocalDefault,
ApolloServerPluginLandingPageProductionDefault,
} from '@apollo/server/plugin/landingPage/default';
import { ApolloDriverConfig } from '@nestjs/apollo';
import { ApolloFastifyContextFunctionArgument } from '@as-integrations/fastify';
import { ApolloDriverConfig as DriverConfig } from '@nestjs/apollo';
import { Injectable } from '@nestjs/common';
import { GqlOptionsFactory } from '@nestjs/graphql';
import { CacheService } from '@seedcompany/cache';
Expand All @@ -29,7 +28,7 @@ export class GraphqlOptions implements GqlOptionsFactory {
private readonly errorFormatter: GraphqlErrorFormatter,
) {}

async createGqlOptions(): Promise<ApolloDriverConfig> {
async createGqlOptions(): Promise<DriverConfig> {
// Apply git hash to Apollo Studio.
// They only look for env, so applying that way.
const version = await this.versionService.version;
Expand All @@ -44,6 +43,7 @@ export class GraphqlOptions implements GqlOptionsFactory {
).asRecord;

return {
path: '/graphql/:opName?',
autoSchemaFile: 'schema.graphql',
context: this.context,
playground: false,
Expand Down Expand Up @@ -80,14 +80,15 @@ export class GraphqlOptions implements GqlOptionsFactory {
};
}

context: ContextFunction<[ExpressContextFunctionArgument], GqlContextType> =
async ({ req, res }) => ({
[isGqlContext.KEY]: true,
request: req,
response: res,
operation: createFakeStubOperation(),
session$: new BehaviorSubject<Session | undefined>(undefined),
});
context = (
...[request, response]: ApolloFastifyContextFunctionArgument
): GqlContextType => ({
[isGqlContext.KEY]: true,
request,
response,
operation: createFakeStubOperation(),
session$: new BehaviorSubject<Session | undefined>(undefined),
});
}

export const createFakeStubOperation = () => {
Expand Down
56 changes: 56 additions & 0 deletions src/core/http/decorators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {
FASTIFY_ROUTE_CONFIG_METADATA,
FASTIFY_ROUTE_CONSTRAINTS_METADATA,
} from '@nestjs/platform-fastify/constants.js';
import { Many } from '@seedcompany/common';
import { createMetadataDecorator } from '@seedcompany/nest';
import { FastifyContextConfig } from 'fastify';
import type { RouteConstraint } from 'fastify/types/route';

export const RouteConstraints = createMetadataDecorator({
key: FASTIFY_ROUTE_CONSTRAINTS_METADATA,
types: ['class', 'method'],
setter: (config: RouteConstraint) => config,
});

export const RouteConfig = createMetadataDecorator({
key: FASTIFY_ROUTE_CONFIG_METADATA,
types: ['class', 'method'],
setter: (config: FastifyContextConfig) => config,
});

/**
* @example
* ```ts
* @RawBody()
* route(
* @Request('rawBody') raw: string,
* @Body() contents: JSON
* ) {}
* ```
* @example
* ```ts
* @RawBody({ passthrough: true })
* route(
* @Body() contents: Buffer
* ) {}
* ```
*/
export const RawBody = createMetadataDecorator({
types: ['class', 'method'],
setter: (
config: {
/**
* Pass the raw body through to the handler or
* just to keep the raw body in addition to regular content parsing.
*/
passthrough?: boolean;
/**
* The allowed content types.
* Only applicable if passthrough is true.
* Defaults to '*'
*/
allowContentTypes?: Many<string> | RegExp;
} = {},
) => config,
});
Loading

0 comments on commit 6980e8f

Please sign in to comment.