Skip to content

Commit 1a780b4

Browse files
feat(realworld-graphql): fix base project (#136)
1 parent d6554d0 commit 1a780b4

File tree

11 files changed

+174
-85
lines changed

11 files changed

+174
-85
lines changed

apps/realworld-graphql/.env.example

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
##== Environment
2-
NODE_ENV=development
1+
##== Environment (local, test, development, staging, production)
2+
NODE_ENV=local
33

44
##== Application
55
APP_NAME="RealWorld GraphQL"
@@ -10,8 +10,6 @@ APP_FALLBACK_LANGUAGE=en
1010
APP_LOG_LEVEL=debug
1111
APP_LOG_SERVICE=console
1212
APP_CORS_ORIGIN=http://localhost:3000,http://example.com
13-
API_PREFIX=api
14-
API_DOCS_ENABLED=true # Should be set to false in production
1513

1614
##== Database
1715
DATABASE_TYPE=postgres

apps/realworld-graphql/src/app.module.ts

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@ import {
1616
I18nModule,
1717
QueryResolver,
1818
} from 'nestjs-i18n';
19-
import path from 'path';
19+
import path, { join } from 'path';
2020
import { DataSource, DataSourceOptions } from 'typeorm';
2121
import { AppResolver } from './app.resolver';
2222
import { AppService } from './app.service';
2323
import { AllConfigType } from './config/config.type';
2424
import { ApiModule } from './modules/api.module';
2525
import authConfig from './modules/auth/config/auth.config';
2626
// import { TypeOrmConfigService } from './database/mysql-typeorm-config.service'; // Uncomment this line if you are using MySQL
27+
import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default';
2728
import { TypeOrmConfigService } from './database/typeorm-config.service';
2829

2930
const configModule = ConfigModule.forRoot({
@@ -71,20 +72,30 @@ const i18nModule = I18nModule.forRootAsync({
7172
inject: [ConfigService],
7273
});
7374

74-
@Module({
75-
imports: [
76-
configModule,
77-
dbModule,
78-
i18nModule,
79-
ApiModule,
80-
GraphQLModule.forRoot<ApolloDriverConfig>({
75+
const graphqlModule = GraphQLModule.forRootAsync<ApolloDriverConfig>({
76+
driver: ApolloDriver,
77+
useFactory: (configService: ConfigService<AllConfigType>) => {
78+
const isLocal: boolean =
79+
configService.get('app.nodeEnv', { infer: true }) === Environment.LOCAL;
80+
return {
8181
driver: ApolloDriver,
82-
autoSchemaFile: true,
82+
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
8383
sortSchema: true,
84-
graphiql: true,
85-
introspection: true,
86-
}),
87-
],
84+
// graphiql: isLocal, // Uncomment this line if you want to use GraphiQL instead of the playground or the Apollo Sandbox
85+
introspection: isLocal,
86+
playground: false,
87+
plugins: [
88+
ApolloServerPluginLandingPageLocalDefault({
89+
embed: isLocal ? true : undefined,
90+
}),
91+
],
92+
};
93+
},
94+
inject: [ConfigService],
95+
});
96+
97+
@Module({
98+
imports: [configModule, dbModule, i18nModule, ApiModule, graphqlModule],
8899
providers: [AppService, AsyncContextProvider, FastifyPinoLogger, AppResolver],
89100
exports: [AsyncContextProvider],
90101
})

apps/realworld-graphql/src/filters/global-exception.filter.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { ErrorCode } from '@/constants/error-code.constant';
33
import { I18nTranslations } from '@/generated/i18n.generated';
44
import {
55
type ArgumentsHost,
6-
Catch,
76
type ExceptionFilter,
87
HttpException,
98
HttpStatus,
@@ -23,7 +22,7 @@ import { I18nContext } from 'nestjs-i18n';
2322
import { EntityNotFoundError, QueryFailedError } from 'typeorm';
2423

2524
// TODO: Will update this filter to handle all exceptions for graphql
26-
@Catch()
25+
// @Catch()
2726
export class GlobalExceptionFilter implements ExceptionFilter {
2827
private i18n: I18nContext<I18nTranslations>;
2928
private readonly logger = new Logger(GlobalExceptionFilter.name);

apps/realworld-graphql/src/main.ts

Lines changed: 48 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,7 @@
11
import compression from '@fastify/compress';
22
import helmet from '@fastify/helmet';
3-
import {
4-
ClassSerializerInterceptor,
5-
HttpStatus,
6-
RequestMethod,
7-
UnprocessableEntityException,
8-
ValidationError,
9-
ValidationPipe,
10-
VersioningType,
11-
} from '@nestjs/common';
123
import { ConfigService } from '@nestjs/config';
13-
import { HttpAdapterHost, NestFactory, Reflector } from '@nestjs/core';
4+
import { NestFactory, Reflector } from '@nestjs/core';
145
import {
156
FastifyAdapter,
167
NestFastifyApplication,
@@ -26,9 +17,6 @@ import {
2617
} from '@repo/nest-common';
2718
import { AppModule } from './app.module';
2819
import { AllConfigType } from './config/config.type';
29-
import { GlobalExceptionFilter } from './filters/global-exception.filter';
30-
import { AuthGuard } from './guards/auth.guard';
31-
import { AuthService } from './modules/auth/auth.service';
3220

3321
async function bootstrap() {
3422
const fastifyAdapter = new FastifyAdapter({
@@ -45,12 +33,28 @@ async function bootstrap() {
4533
},
4634
);
4735

36+
// Get services
37+
const configService = app.get(ConfigService<AllConfigType>);
38+
const reflector = app.get(Reflector);
39+
4840
// Configure the logger
4941
const asyncContext = app.get(AsyncContextProvider);
5042
const logger = new FastifyPinoLogger(
5143
asyncContext,
5244
fastifyAdapter.getInstance().log,
5345
);
46+
47+
// If you want to use the console logger, uncomment the following code
48+
// const logger = new ConsoleLogger({
49+
// ...(configService.getOrThrow('app.nodeEnv', { infer: true }) ===
50+
// Environment.LOCAL && {
51+
// colors: true,
52+
// }),
53+
// ...(configService.getOrThrow('app.nodeEnv', { infer: true }) !==
54+
// Environment.LOCAL && {
55+
// json: true,
56+
// }),
57+
// });
5458
app.useLogger(logger);
5559

5660
fastifyAdapter.getInstance().addHook('onRequest', (request, reply, done) => {
@@ -60,26 +64,27 @@ async function bootstrap() {
6064
}, new Map());
6165
});
6266

63-
const configService = app.get(ConfigService<AllConfigType>);
64-
const reflector = app.get(Reflector);
65-
6667
// Setup security headers
6768
const devContentSecurityPolicy = {
6869
directives: {
69-
defaultSrc: ["'self'"],
70+
defaultSrc: ["'self'", 'https://sandbox.embed.apollographql.com'],
7071
scriptSrc: [
7172
"'self'",
7273
"'unsafe-inline'",
7374
'https://unpkg.com',
74-
'https://cdn.jsdelivr.net',
75+
'https://embeddable-sandbox.cdn.apollographql.com',
7576
],
7677
styleSrc: [
7778
"'self'",
7879
"'unsafe-inline'",
7980
'https://unpkg.com',
80-
'https://cdn.jsdelivr.net',
81+
'https://fonts.googleapis.com',
82+
],
83+
imgSrc: [
84+
"'self'",
85+
'data:',
86+
'https://apollo-server-landing-page.cdn.apollographql.com',
8187
],
82-
imgSrc: ["'self'", 'data:', 'https://cdn.jsdelivr.net'],
8388
},
8489
};
8590

@@ -106,47 +111,29 @@ async function bootstrap() {
106111
});
107112
logger.log(`CORS Origin: ${corsOrigin.toString()}`);
108113

109-
// Use global prefix if you don't have subdomain
110-
app.setGlobalPrefix(
111-
configService.getOrThrow('app.apiPrefix', { infer: true }),
112-
{
113-
exclude: [
114-
// { method: RequestMethod.GET, path: '/' }, // Middeware not working when using exclude by root path https://github.com/nestjs/nest/issues/13401
115-
{ method: RequestMethod.GET, path: 'health' },
116-
],
117-
},
118-
);
119-
120-
app.enableVersioning({
121-
type: VersioningType.URI,
122-
});
123-
124-
app.useGlobalGuards(new AuthGuard(reflector, app.get(AuthService)));
125-
app.useGlobalFilters(
126-
new GlobalExceptionFilter(
127-
app.get(HttpAdapterHost),
128-
configService.getOrThrow('app.debug', { infer: true }),
129-
),
130-
);
131-
app.useGlobalPipes(
132-
new ValidationPipe({
133-
transform: true,
134-
whitelist: true,
135-
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
136-
exceptionFactory: (errors: ValidationError[]) => {
137-
return new UnprocessableEntityException(errors);
138-
},
139-
}),
140-
);
141-
142-
app.useGlobalInterceptors(
143-
new ClassSerializerInterceptor(reflector, {
144-
excludeExtraneousValues: true,
145-
}),
146-
);
147-
148-
// TODO: Config graphql docs
149-
// if (configService.getOrThrow('app.apiDocsEnabled', { infer: true })) {}
114+
// app.useGlobalGuards(new AuthGuard(reflector, app.get(AuthService)));
115+
// app.useGlobalFilters(
116+
// new GlobalExceptionFilter(
117+
// app.get(HttpAdapterHost),
118+
// configService.getOrThrow('app.debug', { infer: true }),
119+
// ),
120+
// );
121+
// app.useGlobalPipes(
122+
// new ValidationPipe({
123+
// transform: true,
124+
// whitelist: true,
125+
// errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
126+
// exceptionFactory: (errors: ValidationError[]) => {
127+
// return new UnprocessableEntityException(errors);
128+
// },
129+
// }),
130+
// );
131+
132+
// app.useGlobalInterceptors(
133+
// new ClassSerializerInterceptor(reflector, {
134+
// excludeExtraneousValues: true,
135+
// }),
136+
// );
150137

151138
await app.listen(
152139
configService.getOrThrow('app.port', { infer: true }) as number,
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Field, InputType } from '@nestjs/graphql';
2+
3+
@InputType()
4+
export class CreateUserInput {
5+
@Field(() => String)
6+
email: string;
7+
}
8+
9+
@InputType()
10+
export class UpdateUserInput {
11+
@Field(() => String)
12+
email: string;
13+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Field, ObjectType } from '@nestjs/graphql';
2+
3+
@ObjectType()
4+
export class User {
5+
@Field(() => String)
6+
email: string;
7+
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Module } from '@nestjs/common';
2-
2+
import { UserResolver } from './user.resolver';
3+
import { UserService } from './user.service';
34
@Module({
45
imports: [],
5-
controllers: [],
6-
providers: [],
6+
providers: [UserResolver, UserService],
77
})
88
export class UserModule {}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
2+
import { CreateUserInput, UpdateUserInput } from './dto/user.dto';
3+
import { User } from './model/user.dto';
4+
import { UserService } from './user.service';
5+
6+
@Resolver()
7+
export class UserResolver {
8+
constructor(private readonly userService: UserService) {}
9+
10+
@Query(() => User)
11+
async currentUser(@Context() context: any) {
12+
const user = context.req.user;
13+
return user;
14+
}
15+
16+
@Mutation(() => User)
17+
async createUser(@Args('user') user: CreateUserInput): Promise<User> {
18+
const createdUser = await this.userService.createUser(user);
19+
return createdUser;
20+
}
21+
22+
@Mutation(() => User)
23+
async updateUser(@Args('user') user: UpdateUserInput): Promise<User> {
24+
const updatedUser = await this.userService.updateUser(user);
25+
return updatedUser;
26+
}
27+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Injectable, Logger } from '@nestjs/common';
2+
import { CreateUserInput, UpdateUserInput } from './dto/user.dto';
3+
4+
@Injectable()
5+
export class UserService {
6+
private readonly logger = new Logger(UserService.name);
7+
8+
async updateUser(user: UpdateUserInput) {
9+
this.logger.log(`Updating user: ${user.email}`);
10+
return user;
11+
}
12+
13+
async createUser(user: CreateUserInput) {
14+
this.logger.log(`Creating user: ${user.email}`);
15+
return user;
16+
}
17+
}

apps/realworld-graphql/src/schema.gql

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# ------------------------------------------------------
2+
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
3+
# ------------------------------------------------------
4+
5+
input CreateUserInput {
6+
email: String!
7+
}
8+
9+
type Mutation {
10+
createUser(user: CreateUserInput!): User!
11+
updateUser(user: UpdateUserInput!): User!
12+
}
13+
14+
type Query {
15+
currentUser: User!
16+
getHello: String!
17+
}
18+
19+
input UpdateUserInput {
20+
email: String!
21+
}
22+
23+
type User {
24+
email: String!
25+
}

packages/nest-common/src/fastify-pino-logger/fastify-pino.util.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import hyperid from 'hyperid';
33

44
export const REQUEST_ID_HEADER = 'X-Request-Id';
55

6-
export type FastifyLoggerEnv = 'development' | 'staging' | 'production';
6+
export type FastifyLoggerEnv =
7+
| 'local'
8+
| 'development'
9+
| 'staging'
10+
| 'production';
711

812
const developmentLogger = (): any => {
913
return {
@@ -69,12 +73,13 @@ export function fastifyPinoOptions(
6973
env: FastifyLoggerEnv,
7074
): (FastifyLoggerOptions & PinoLoggerOptions) | boolean {
7175
const envToLogger = {
76+
local: developmentLogger(),
7277
development: developmentLogger(),
7378
production: {
7479
level: 'debug',
7580
},
7681
staging: {
77-
level: 'info',
82+
level: 'debug',
7883
},
7984
} as const;
8085

0 commit comments

Comments
 (0)