Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(backend): migrate to nestjs #68

Merged
merged 10 commits into from
Jun 3, 2024
Prev Previous commit
Next Next commit
chore(backend): setup and implement graphql resources
  • Loading branch information
tericcabrel committed May 26, 2024
commit 6737d64cf5e5e6aa15f8bb270f4c1d182e5f2b07
2 changes: 1 addition & 1 deletion apps/backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ NODE_ENV=development
APP_VERSION=1.0.0
HOST=http:https://localhost
PORT=7501
ENABLE_INTROSPECTION=true
INTROSPECTION_ENABLED=true
DATABASE_URL="mysql:https://root:@127.0.0.1:3311/snipcode"
ADMIN_PASSWORD=nwHSvXuQxjey43Bp
CONVERTKIT_API_KEY=convertKitApiKey
Expand Down
3 changes: 2 additions & 1 deletion apps/backend/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ module.exports = {
extends: '../../.eslintrc.json',
ignorePatterns: [
'jest.config.ts',
'.eslintrc.js'
'.eslintrc.js',
'graphql.schema.ts'
],
parserOptions: {
ecmaVersion: 2023,
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ export type EnvironmentVariables = {
CONVERTKIT_FORM_ID: string;
CONVERTKIT_TAG_ID: string;
DATABASE_URL: string;
ENABLE_INTROSPECTION: string;
GITHUB_CLIENT_ID: string;
GITHUB_CLIENT_SECRET: string;
HOST: string;
INTROSPECTION_ENABLED: string;
NODE_ENV: string;
PORT: string;
REQUEST_TIMEOUT: string;
Expand Down
10 changes: 8 additions & 2 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,27 @@
"scripts": {
"build": "nest build",
"clean": "rm -rf .turbo dist build logs",
"dev": "nest start --watch",
"dev": "dotenv -e .env.local -- nest start --watch",
"dev:debug": "nest start --debug --watch",
"gql:gen:types": "ts-node scripts/generate-graphql-types.ts",
"lint": "eslint \"{src,scripts}/**/*.ts\" --fix",
"prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest --watch",
"test:coverage": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:it": "jest integration.spec.ts"
},
"dependencies": {
"@apollo/server": "4.10.4",
"@nestjs/apollo": "12.1.0",
"@nestjs/common": "10.3.8",
"@nestjs/config": "3.2.2",
"@nestjs/core": "10.3.8",
"@nestjs/graphql": "12.1.1",
"@nestjs/platform-express": "10.3.8",
"@prisma/client": "5.14.0",
"@sentry/node": "8.3.0",
"graphql": "16.8.1",
"reflect-metadata": "0.2.2",
"rxjs": "7.8.1",
"zod": "3.23.8"
Expand All @@ -37,6 +42,7 @@
"source-map-support": "0.5.21",
"supertest": "7.0.0",
"ts-loader": "9.5.1",
"ts-morph": "22.0.0",
"ts-node": "10.9.2",
"tsconfig-paths": "4.2.0"
}
Expand Down
14 changes: 14 additions & 0 deletions apps/backend/scripts/generate-graphql-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { join } from 'path';

import { GraphQLDefinitionsFactory } from '@nestjs/graphql';

const definitionsFactory = new GraphQLDefinitionsFactory();

void definitionsFactory.generate({
defaultScalarType: 'unknown',
emitTypenameField: true,
enumsAsTypes: true,
outputAs: 'interface',
path: join(process.cwd(), 'src/types/graphql.schema.ts'),
typePaths: ['./src/features/**/*.graphql'],
});
40 changes: 35 additions & 5 deletions apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
import { ApolloServerPlugin } from '@apollo/server';
import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Logger, Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { GraphQLModule } from '@nestjs/graphql';
import { DomainModule } from '@snipcode/domain';

import { AppController } from './app.controller';
import { AppService } from './app.service';
import appConfig, { EnvironmentVariables, validate } from './configs/environment';
import { EnvironmentVariables, validate } from './configs/environment';
import { AuthFeatureModule } from './features/auth/auth.module';
import { FolderFeatureModule } from './features/folders/folder.module';
import { SnippetFeatureModule } from './features/snippets/snippet.module';
import { UserFeatureModule } from './features/users/user.module';
import { IS_DEV } from './utils/environment';
import { DateScalar } from './utils/graphql/date-scalar';

const explorerPlugin: ApolloServerPlugin[] = IS_DEV ? [ApolloServerPluginLandingPageLocalDefault({ embed: true })] : [];

@Module({
controllers: [AppController],
imports: [
ConfigModule.forRoot({
envFilePath: ['.env.local', '.env.test'],
isGlobal: true,
load: [appConfig],
validate,
}),
DomainModule.forRootAsync({
Expand All @@ -21,14 +32,33 @@ import appConfig, { EnvironmentVariables, validate } from './configs/environment
useFactory: (configService: ConfigService<EnvironmentVariables, true>) => {
return {
convertKit: {
apiKey: '',
formId: '',
apiKey: configService.get('CONVERTKIT_API_KEY'),
formId: configService.get('CONVERTKIT_FORM_ID'),
},
databaseUrl: configService.get('DATABASE_URL'),
};
},
}),
GraphQLModule.forRootAsync<ApolloDriverConfig>({
driver: ApolloDriver,
inject: [ConfigService],
useFactory: (configService: ConfigService<EnvironmentVariables, true>) => {
return {
cache: 'bounded',
includeStacktraceInErrorResponses: IS_DEV,
introspection: configService.get('INTROSPECTION_ENABLED'),
nodeEnv: configService.get('NODE_ENV'),
playground: false,
plugins: [...explorerPlugin],
typePaths: ['./src/features/**/*.graphql'],
};
},
}),
AuthFeatureModule,
FolderFeatureModule,
SnippetFeatureModule,
UserFeatureModule,
],
providers: [Logger, AppService],
providers: [Logger, DateScalar, AppService],
})
export class AppModule {}
36 changes: 36 additions & 0 deletions apps/backend/src/configs/auth.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException, createParamDecorator } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { SessionService } from '@snipcode/domain';
import { errors } from '@snipcode/utils';

import { GraphQLContext } from '../types/common';

@Injectable()
export class AuthGuard implements CanActivate {
constructor(private sessionService: SessionService) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const ctx = GqlExecutionContext.create(context);
const request = ctx.getContext<GraphQLContext>().req;

const token = request.headers.authorization;

if (!token) {
throw new UnauthorizedException(errors.NOT_AUTHENTICATED);
}

const session = await this.sessionService.findByToken(token);

if (!session) {
throw new UnauthorizedException(errors.NOT_AUTHENTICATED);
}

request['userId'] = session.userId;

return true;
}
}

export const UserId = createParamDecorator(
(_data: unknown, ctx: ExecutionContext) => GqlExecutionContext.create(ctx).getContext<GraphQLContext>().req.userId,
);
18 changes: 5 additions & 13 deletions apps/backend/src/configs/environment.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,18 @@
import { registerAs } from '@nestjs/config';
import { z } from 'zod';

export default registerAs('app', () => ({
databaseUrl: process.env.DATABASE_URL,
env: process.env.NODE_ENV,
host: process.env.HOST,
port: parseInt(process.env.PORT ?? '7501', 10),
sentry: {
dsn: process.env.SENTRY_DSN,
enabled: process.env.SENTRY_ENABLED,
},
version: process.env.APP_VERSION,
}));

const EnvironmentVariablesSchema = z.object({
APP_VERSION: z.string(),
CONVERTKIT_API_KEY: z.string(),
CONVERTKIT_FORM_ID: z.string(),
CONVERTKIT_TAG_ID: z.string(),
DATABASE_URL: z.string(),
HOST: z.string(),
INTROSPECTION_ENABLED: z.boolean({ coerce: true }),
NODE_ENV: z.union([z.literal('development'), z.literal('production'), z.literal('test')]),
PORT: z.number({ coerce: true }).min(7000).max(8000),
SENTRY_DSN: z.string(),
SENTRY_ENABLED: z.boolean({ coerce: true }),
SESSION_LIFETIME: z.number({ coerce: true }).min(1),
});

export type EnvironmentVariables = z.infer<typeof EnvironmentVariablesSchema>;
Expand Down
30 changes: 30 additions & 0 deletions apps/backend/src/configs/exception.filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ArgumentsHost, Catch } from '@nestjs/common';
import { AbstractHttpAdapter, BaseExceptionFilter } from '@nestjs/core';
import { GqlArgumentsHost, GqlContextType } from '@nestjs/graphql';
import { isAppError } from '@snipcode/utils';
import { GraphQLError } from 'graphql';

@Catch()
export class ApplicationExceptionFilter extends BaseExceptionFilter {
constructor(httpAdapter: AbstractHttpAdapter) {
super(httpAdapter);
}

catch(exception: unknown, host: ArgumentsHost) {
const gqlHost = GqlArgumentsHost.create(host);

if (gqlHost.getType<GqlContextType>() === 'graphql') {
if (isAppError(exception)) {
throw new GraphQLError(exception.message, {
extensions: {
code: exception.code,
},
originalError: exception,
});
}
} else {
// Handle HTTP exceptions
super.catch(exception, host);
}
}
}
4 changes: 3 additions & 1 deletion apps/backend/src/configs/instrument.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import * as Sentry from '@sentry/node';

import { IS_DEV, IS_PROD } from '../utils/environment';

const isSentryEnabled = () => {
if (process.env.SENTRY_ENABLED !== 'true') {
return false;
}

return process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'development';
return IS_PROD || IS_DEV;
};

Sentry.init({
Expand Down
8 changes: 8 additions & 0 deletions apps/backend/src/features/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';

import { AuthResolvers } from './graphql/auth.resolvers';

@Module({
providers: [AuthResolvers],
})
export class AuthFeatureModule {}
Loading