Skip to content

Commit d91e731

Browse files
authored
feat(Feat/get condition directly): Change the way select and join are retrieved from the GraphQL query. (#35)
* Chore * Chore: Change string to symbol for metadata key * Feat: Implement repository interceptor to get select and relations - Change from util function to this * Chore: Remove lodash * Docs: Update read me * Feat: Change from sending repository to entity in interceptor * Chore: Change filename * Chore: Change function name * Feat: Wrtie test code * Feat: Update plop template file * Chore: Remove useless * Chore: Change from string to symbol as key of metadata * Chore: Remark no use
1 parent 166e927 commit d91e731

25 files changed

+1176
-1075
lines changed

README.md

+14-7
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,20 @@ You can solve them with Sending JWT token in `Http Header` with the `Authorizati
6161
}
6262
```
6363

64+
### GraphQL Query To Select and relations
65+
66+
#### Dynamic Query Optimization
67+
68+
- Automatically maps GraphQL queries to optimized SELECT and JOIN clauses in TypeORM.
69+
70+
- Ensures that only the requested fields and necessary relations are retrieved, reducing over-fetching and improving performance.
71+
72+
- With using interceptor (name: `UseRepositoryInterceptor`) and paramDecorator (name: `GraphQLQueryToOption`)
73+
74+
#### How to use
75+
76+
- You can find example code in [/src/user/user.resolver.ts](/src/user/user.resolver.ts)
77+
6478
#### Example of some protected GraphQL
6579

6680
- getMe (must be authenticated)
@@ -342,13 +356,6 @@ db.public.registerFunction({
342356
- [x] Integration Test (Use in-memory DB)
343357
- [x] End To End Test (Use docker)
344358
345-
- [ ] Add Many OAUths (Both of front and back end)
346-
347-
- [ ] Kakao
348-
- [ ] Google
349-
- [ ] Apple
350-
- [ ] Naver
351-
352359
- [x] CI
353360
354361
- [x] Github actions

generator/templates/resolver.hbs

+15-7
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,39 @@ import { UseAuthGuard } from 'src/common/decorators/auth-guard.decorator';
22
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'
33
import { {{pascalCase tableName}}Service } from './{{tableName}}.service'
44
import { GetManyInput, GetOneInput } from 'src/common/graphql/custom.input'
5-
import { CurrentQuery } from 'src/common/decorators/query.decorator'
65
import GraphQLJSON from 'graphql-type-json';
76

7+
import { GetInfoFromQueryProps } from 'src/common/graphql/utils/types';
8+
import { GraphQLQueryToOption } from 'src/common/decorators/option.decorator';
9+
import { UseRepositoryInterceptor } from 'src/common/decorators/repository-interceptor.decorator';
10+
811
import { Get{{pascalCase tableName}}Type, {{pascalCase tableName}} } from './entities/{{tableName}}.entity';
912
import { Create{{pascalCase tableName}}Input, Update{{pascalCase tableName}}Input } from './inputs/{{tableName}}.input';
1013

1114
@Resolver()
1215
export class {{pascalCase tableName}}Resolver {
1316
constructor(private readonly {{tableName}}Service: {{pascalCase tableName}}Service) {}
17+
1418
@Query(() => Get{{pascalCase tableName}}Type)
1519
@UseAuthGuard('admin')
20+
@UseRepositoryInterceptor({{pascalCase tableName}})
1621
getMany{{pascalCase tableName}}List(
17-
@Args({ name: 'input', nullable: true }) qs: GetManyInput<{{pascalCase tableName}}>,
18-
@CurrentQuery() gqlQuery: string,
22+
@Args({ name: 'input', nullable: true }) condition: GetManyInput<{{pascalCase tableName}}>,
23+
@GraphQLQueryToOption<{{pascalCase tableName}}>(true)
24+
option: GetInfoFromQueryProps<{{pascalCase tableName}}>,
1925
) {
20-
return this.{{tableName}}Service.getMany(qs, gqlQuery);
26+
return this.{{tableName}}Service.getMany({ ...condition, ...option });
2127
}
2228

2329
@Query(() => {{pascalCase tableName}})
2430
@UseAuthGuard('admin')
31+
@UseRepositoryInterceptor({{pascalCase tableName}})
2532
getOne{{pascalCase tableName}}(
26-
@Args({ name: 'input' }) qs: GetOneInput<{{pascalCase tableName}}>,
27-
@CurrentQuery() gqlQuery: string,
33+
@Args({ name: 'input' }) condition: GetOneInput<{{pascalCase tableName}}>,
34+
@GraphQLQueryToOption<{{pascalCase tableName}}>()
35+
option: GetInfoFromQueryProps<{{pascalCase tableName}}>,
2836
) {
29-
return this.{{tableName}}Service.getOne(qs, gqlQuery);
37+
return this.{{tableName}}Service.getOne({ ...condition, ...option });
3038
}
3139

3240
@Mutation(() => {{pascalCase tableName}})

generator/templates/resolver.spec.hbs

+21-26
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { GetManyInput, GetOneInput } from 'src/common/graphql/custom.input'
99
import { {{pascalCase tableName}} } from './entities/{{tableName}}.entity'
1010
import { UtilModule } from 'src/common/shared/services/util.module';
1111
import { UtilService } from 'src/common/shared/services/util.service';
12+
import { DataSource } from 'typeorm';
1213

1314
import { Create{{pascalCase tableName}}Input, Update{{pascalCase tableName}}Input } from './inputs/{{tableName}}.input'
1415

@@ -26,6 +27,10 @@ describe('{{pascalCase tableName}}Resolver', () => {
2627
provide: {{pascalCase tableName}}Service,
2728
useFactory: MockServiceFactory.getMockService({{pascalCase tableName}}Service),
2829
},
30+
{
31+
provide: DataSource,
32+
useValue: undefined,
33+
},
2934
],
3035
}).compile()
3136

@@ -39,7 +44,7 @@ describe('{{pascalCase tableName}}Resolver', () => {
3944
})
4045

4146
it('Calling "Get many {{tableName}} list" method', () => {
42-
const qs: GetManyInput<{{pascalCase tableName}}> = {
47+
const condition: GetManyInput<{{pascalCase tableName}}> = {
4348
where: {
4449
{{#if (is "increment" idType)}}
4550
id: utilService.getRandomNumber(0,999999)
@@ -49,22 +54,17 @@ describe('{{pascalCase tableName}}Resolver', () => {
4954
},
5055
}
5156

52-
const gqlQuery = `
53-
query GetMany{{pascalCase tableName}}List {
54-
getMany{{pascalCase tableName}}List {
55-
data {
56-
id
57-
}
58-
}
59-
}
60-
`
61-
62-
expect(resolver.getMany{{pascalCase tableName}}List(qs, gqlQuery)).not.toEqual(null)
63-
expect(mockedService.getMany).toHaveBeenCalledWith(qs, gqlQuery)
57+
const option = { relations: undefined, select: undefined };
58+
59+
expect(resolver.getMany{{pascalCase tableName}}List(condition, option)).not.toEqual(null)
60+
expect(mockedService.getMany).toHaveBeenCalledWith({
61+
...condition,
62+
...option,
63+
})
6464
})
6565

6666
it('Calling "Get one {{tableName}} list" method', () => {
67-
const qs: GetOneInput<{{pascalCase tableName}}> = {
67+
const condition: GetOneInput<{{pascalCase tableName}}> = {
6868
where: {
6969
{{#if (is "increment" idType)}}
7070
id: utilService.getRandomNumber(0,999999)
@@ -74,18 +74,13 @@ describe('{{pascalCase tableName}}Resolver', () => {
7474
},
7575
}
7676

77-
const gqlQuery = `
78-
query GetOne{{pascalCase tableName}} {
79-
getOne{{pascalCase tableName}} {
80-
data {
81-
id
82-
}
83-
}
84-
}
85-
`
86-
87-
expect(resolver.getOne{{pascalCase tableName}}(qs, gqlQuery)).not.toEqual(null)
88-
expect(mockedService.getOne).toHaveBeenCalledWith(qs, gqlQuery)
77+
const option = { relations: undefined, select: undefined };
78+
79+
expect(resolver.getOne{{pascalCase tableName}}(condition, option)).not.toEqual(null)
80+
expect(mockedService.getOne).toHaveBeenCalledWith({
81+
...condition,
82+
...option,
83+
})
8984
})
9085

9186
it('Calling "Create {{tableName}}" method', () => {

generator/templates/service.hbs

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import { Injectable } from '@nestjs/common'
2+
23
import { OneRepoQuery, RepoQuery } from 'src/common/graphql/types'
4+
35
import { {{pascalCase tableName}}Repository } from './{{tableName}}.repository'
46
import { {{pascalCase tableName}} } from './entities/{{tableName}}.entity';
57
import { Create{{pascalCase tableName}}Input, Update{{pascalCase tableName}}Input } from './inputs/{{tableName}}.input';
68

79
@Injectable()
810
export class {{pascalCase tableName}}Service {
911
constructor(private readonly {{tableName}}Repository: {{pascalCase tableName}}Repository) {}
10-
getMany(qs: RepoQuery<{{pascalCase tableName}}> = {}, gqlQuery?: string) {
11-
return this.{{tableName}}Repository.getMany(qs, gqlQuery);
12+
getMany(option?: RepoQuery<{{pascalCase tableName}}>) {
13+
return this.{{tableName}}Repository.getMany(option);
1214
}
1315

14-
getOne(qs: OneRepoQuery<{{pascalCase tableName}}>, gqlQuery?: string) {
15-
return this.{{tableName}}Repository.getOne(qs, gqlQuery);
16+
getOne(option: OneRepoQuery<{{pascalCase tableName}}>) {
17+
return this.{{tableName}}Repository.getOne(option);
1618
}
1719

1820
create(input: Create{{pascalCase tableName}}Input) {

generator/templates/service.spec.hbs

+4-4
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ describe('{{pascalCase tableName}}Service', () => {
4242
})
4343

4444
it('Calling "Get many" method', () => {
45-
const qs: RepoQuery<{{pascalCase tableName}}> = {
45+
const option: RepoQuery<{{pascalCase tableName}}> = {
4646
where: {
4747
{{#if (is "increment" idType)}}
4848
id: utilService.getRandomNumber(0,999999)
@@ -52,12 +52,12 @@ describe('{{pascalCase tableName}}Service', () => {
5252
},
5353
}
5454

55-
expect(service.getMany(qs)).not.toEqual(null)
55+
expect(service.getMany(option)).not.toEqual(null)
5656
expect(mockedRepository.getMany).toHaveBeenCalled()
5757
})
5858

5959
it('Calling "Get one" method', () => {
60-
const qs: OneRepoQuery<{{pascalCase tableName}}> = {
60+
const option: OneRepoQuery<{{pascalCase tableName}}> = {
6161
where: {
6262
{{#if (is "increment" idType)}}
6363
id: utilService.getRandomNumber(0,999999)
@@ -67,7 +67,7 @@ describe('{{pascalCase tableName}}Service', () => {
6767
},
6868
}
6969

70-
expect(service.getOne(qs)).not.toEqual(null)
70+
expect(service.getOne(option)).not.toEqual(null)
7171
expect(mockedRepository.getOne).toHaveBeenCalled()
7272
})
7373

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@
8282
"@types/express": "^5.0.0",
8383
"@types/graphql-upload": "^15.0.2",
8484
"@types/jest": "29.5.14",
85-
"@types/lodash": "^4.17.13",
8685
"@types/node": "^22.10.3",
8786
"@types/passport-jwt": "^4.0.1",
8887
"@types/passport-local": "^1.0.38",

src/auth/strategies/jwt.strategy.ts

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
2424
try {
2525
const userData = await this.userService.getOne({
2626
where: { id: payload.id },
27+
select: { id: true, role: true },
2728
});
2829

2930
done(null, userData);

src/cache/custom-cache.module.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { CacheModule, CacheModuleOptions } from '@nestjs/cache-manager';
22
import { DynamicModule, Module, OnModuleInit } from '@nestjs/common';
33
import { APP_INTERCEPTOR, DiscoveryModule } from '@nestjs/core';
44

5-
import { CustomCacheInterceptor } from './custom-cache-interceptor';
5+
import { CustomCacheInterceptor } from './custom-cache.interceptor';
66
import { CustomCacheService } from './custom-cache.service';
77

88
@Module({})

src/common/decorators/auth-guard.decorator.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import { SetMetadata, UseGuards, applyDecorators } from '@nestjs/common';
22

33
import { GraphqlPassportAuthGuard } from '../guards/graphql-passport-auth.guard';
44

5+
export const GUARD_ROLE = Symbol('GUARD_ROLE');
6+
57
export const UseAuthGuard = (roles?: string | string[]) =>
68
applyDecorators(
79
SetMetadata(
8-
'roles',
10+
GUARD_ROLE,
911
roles ? (Array.isArray(roles) ? roles : [roles]) : ['user'],
1012
),
1113
UseGuards(GraphqlPassportAuthGuard),

src/common/graphql/utils/getConditionFromGqlQuery.ts renamed to src/common/decorators/option.decorator.ts

+75-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
1+
import {
2+
ExecutionContext,
3+
InternalServerErrorException,
4+
createParamDecorator,
5+
} from '@nestjs/common';
6+
import { GqlExecutionContext } from '@nestjs/graphql';
7+
8+
import { parse, print } from 'graphql';
19
import { Repository } from 'typeorm';
210

3-
import { set } from './processWhere';
4-
import { AddKeyValueInObjectProps, GetInfoFromQueryProps } from './types';
11+
import { set } from '../graphql/utils/processWhere';
12+
import {
13+
AddKeyValueInObjectProps,
14+
GetInfoFromQueryProps,
15+
} from '../graphql/utils/types';
516

617
const DATA = 'data';
718

@@ -32,7 +43,7 @@ const addKeyValuesInObject = <Entity>({
3243
return { relations, select };
3344
};
3445

35-
export function getConditionFromGqlQuery<Entity>(
46+
export function getOptionFromGqlQuery<Entity>(
3647
this: Repository<Entity>,
3748
query: string,
3849
hasCountType?: boolean,
@@ -108,3 +119,64 @@ export function getConditionFromGqlQuery<Entity>(
108119
},
109120
);
110121
}
122+
123+
const getCurrentGraphQLQuery = (ctx: GqlExecutionContext) => {
124+
const { fieldName, path } = ctx.getArgByIndex(3) as {
125+
fieldName: string;
126+
path: { key: string };
127+
};
128+
129+
const query = ctx.getContext().req.body.query;
130+
const operationJson = print(parse(query));
131+
const operationArray = operationJson.split('\n');
132+
133+
operationArray.shift();
134+
operationArray.pop();
135+
136+
const firstLineFinder = operationArray.findIndex((v) =>
137+
v.includes(fieldName === path.key ? fieldName : path.key + ':'),
138+
);
139+
140+
operationArray.splice(0, firstLineFinder);
141+
142+
const stack = [];
143+
144+
let depth = 0;
145+
146+
for (const line of operationArray) {
147+
stack.push(line);
148+
if (line.includes('{')) {
149+
depth++;
150+
} else if (line.includes('}')) {
151+
depth--;
152+
}
153+
154+
if (depth === 0) {
155+
break;
156+
}
157+
}
158+
159+
return stack.join('\n');
160+
};
161+
162+
export const GraphQLQueryToOption = <T>(hasCountType?: boolean) =>
163+
createParamDecorator((_: unknown, context: ExecutionContext) => {
164+
const ctx = GqlExecutionContext.create(context);
165+
const request = ctx.getContext().req;
166+
const query = getCurrentGraphQLQuery(ctx);
167+
const repository: Repository<T> = request.repository;
168+
169+
if (!repository) {
170+
throw new InternalServerErrorException(
171+
"Repository not found in request, don't forget to use UseRepositoryInterceptor",
172+
);
173+
}
174+
175+
const queryOption: GetInfoFromQueryProps<T> = getOptionFromGqlQuery.call(
176+
repository,
177+
query,
178+
hasCountType,
179+
);
180+
181+
return queryOption;
182+
})();

0 commit comments

Comments
 (0)