Skip to content

Commit 7d0438a

Browse files
author
Martin Kagamino Lehoux
committed
feat(graphql): type check field default value
1 parent e322ecb commit 7d0438a

File tree

5 files changed

+74
-20
lines changed

5 files changed

+74
-20
lines changed

packages/graphql/lib/decorators/field.decorator.ts

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,19 @@ import { Type } from '@nestjs/common';
99
import { isFunction } from '@nestjs/common/utils/shared.utils';
1010
import { Complexity, FieldMiddleware } from '../interfaces';
1111
import { BaseTypeOptions } from '../interfaces/base-type-options.interface';
12-
import { ReturnTypeFunc } from '../interfaces/return-type-func.interface';
12+
import {
13+
GqlTypeReference,
14+
ReturnTypeFunc,
15+
ReturnTypeFuncValue,
16+
} from '../interfaces/return-type-func.interface';
1317
import { LazyMetadataStorage } from '../schema-builder/storages/lazy-metadata.storage';
1418
import { TypeMetadataStorage } from '../schema-builder/storages/type-metadata.storage';
1519
import { reflectTypeFromMetadata } from '../utils/reflection.utilts';
1620

1721
/**
1822
* Interface defining options that can be passed to `@Field()` decorator.
1923
*/
20-
export interface FieldOptions extends BaseTypeOptions {
24+
export interface FieldOptions<T = any> extends BaseTypeOptions<T> {
2125
/**
2226
* Name of the field.
2327
*/
@@ -40,6 +44,12 @@ export interface FieldOptions extends BaseTypeOptions {
4044
middleware?: FieldMiddleware[];
4145
}
4246

47+
type FieldOptionsExtractor<T> = T extends [GqlTypeReference<infer P>]
48+
? FieldOptions<P[]>
49+
: T extends GqlTypeReference<infer P>
50+
? FieldOptions<P>
51+
: never;
52+
4353
/**
4454
* @Field() decorator is used to mark a specific class property as a GraphQL field.
4555
* Only properties decorated with this decorator will be defined in the schema.
@@ -49,24 +59,25 @@ export function Field(): PropertyDecorator & MethodDecorator;
4959
* @Field() decorator is used to mark a specific class property as a GraphQL field.
5060
* Only properties decorated with this decorator will be defined in the schema.
5161
*/
52-
export function Field(
53-
options: FieldOptions,
62+
export function Field<T extends ReturnTypeFuncValue>(
63+
options: FieldOptionsExtractor<T>,
5464
): PropertyDecorator & MethodDecorator;
5565
/**
5666
* @Field() decorator is used to mark a specific class property as a GraphQL field.
5767
* Only properties decorated with this decorator will be defined in the schema.
5868
*/
59-
export function Field(
60-
returnTypeFunction?: ReturnTypeFunc,
61-
options?: FieldOptions,
69+
export function Field<T extends ReturnTypeFuncValue>(
70+
returnTypeFunction?: ReturnTypeFunc<T>,
71+
options?: FieldOptionsExtractor<T>,
6272
): PropertyDecorator & MethodDecorator;
73+
6374
/**
6475
* @Field() decorator is used to mark a specific class property as a GraphQL field.
6576
* Only properties decorated with this decorator will be defined in the schema.
6677
*/
67-
export function Field(
68-
typeOrOptions?: ReturnTypeFunc | FieldOptions,
69-
fieldOptions?: FieldOptions,
78+
export function Field<T extends ReturnTypeFuncValue>(
79+
typeOrOptions?: ReturnTypeFunc<T> | FieldOptionsExtractor<T>,
80+
fieldOptions?: FieldOptionsExtractor<T>,
7081
): PropertyDecorator & MethodDecorator {
7182
return (
7283
prototype: Object,
@@ -83,9 +94,9 @@ export function Field(
8394
};
8495
}
8596

86-
export function addFieldMetadata(
87-
typeOrOptions: ReturnTypeFunc | FieldOptions,
88-
fieldOptions: FieldOptions,
97+
export function addFieldMetadata<T extends ReturnTypeFuncValue>(
98+
typeOrOptions: ReturnTypeFunc<T> | FieldOptionsExtractor<T>,
99+
fieldOptions: FieldOptionsExtractor<T>,
89100
prototype: Object,
90101
propertyKey?: string,
91102
descriptor?: TypedPropertyDescriptor<any>,
@@ -103,7 +114,7 @@ export function addFieldMetadata(
103114
metadataKey: isResolverMethod ? 'design:returntype' : 'design:type',
104115
prototype,
105116
propertyKey,
106-
explicitTypeFn: typeFunc as ReturnTypeFunc,
117+
explicitTypeFn: typeFunc as ReturnTypeFunc<T>,
107118
typeOptions: options,
108119
});
109120

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
export type NullableList = 'items' | 'itemsAndList';
2-
export interface BaseTypeOptions {
2+
export interface BaseTypeOptions<T = any> {
33
/**
44
* Determines whether field/argument/etc is nullable.
55
*/
66
nullable?: boolean | NullableList;
77
/**
88
* Default value.
99
*/
10-
defaultValue?: any;
10+
defaultValue?: T;
1111
}
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { Type } from '@nestjs/common';
22
import { GraphQLScalarType } from 'graphql';
33

4-
export type GqlTypeReference =
5-
| Type<any>
4+
export type GqlTypeReference<T = any> =
5+
| Type<T>
66
| GraphQLScalarType
77
| Function
88
| object
99
| symbol;
1010
export type ReturnTypeFuncValue = GqlTypeReference | [GqlTypeReference];
11-
export type ReturnTypeFunc = (returns?: void) => ReturnTypeFuncValue;
11+
export type ReturnTypeFunc<T extends ReturnTypeFuncValue = any> = (
12+
returns?: void,
13+
) => T;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { BaseTypeOptions } from './base-type-options.interface';
22

3-
export interface TypeOptions extends BaseTypeOptions {
3+
export interface TypeOptions<T = any> extends BaseTypeOptions<T> {
44
isArray?: boolean;
55
arrayDepth?: number;
66
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Field } from '../../../lib/decorators';
2+
3+
class Inner {
4+
test: string;
5+
}
6+
7+
// It should expect the right primitive as defaultValue
8+
class WrongPrimitive {
9+
// @ts-expect-error The defaultValue should be a boolean
10+
@Field(() => Boolean, { defaultValue: 'true' })
11+
bool: boolean;
12+
}
13+
14+
class CorrectPrimitive {
15+
@Field(() => Boolean, { defaultValue: true })
16+
bool: boolean;
17+
}
18+
19+
// It should expect the right object as defaultValue
20+
class WrongObject {
21+
// @ts-expect-error The defaultValue should be of the shape of Inner
22+
@Field(() => Inner, { defaultValue: { success: true } })
23+
inner: Inner;
24+
}
25+
26+
class CorrectObject {
27+
@Field(() => Inner, { defaultValue: { test: 'hello' } })
28+
inner: Inner;
29+
}
30+
31+
// It should expect an Array as defaultValue
32+
class WrongArray {
33+
// @ts-expect-error The defaultValue should be an Array
34+
@Field(() => [Inner], { defaultValue: { test: 'test' } })
35+
inners: Inner[];
36+
}
37+
38+
class CorrectArray {
39+
@Field(() => [Inner], { defaultValue: [{ test: 'test' }] })
40+
inner: Inner;
41+
}

0 commit comments

Comments
 (0)