Skip to content

Commit

Permalink
feat: Implement support for @_optional and @_required on fields (#32
Browse files Browse the repository at this point in the history
)
  • Loading branch information
kitten authored Jan 22, 2024
1 parent e064743 commit d29ed37
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 4 deletions.
7 changes: 7 additions & 0 deletions .changeset/fair-pillows-repair.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'gql.tada': minor
---

Support `@_optional` and `@_required` directives on fields overriding the field types.
When used, `@_required` can turn a nullable type into a non-nullable, and `@_optional`
can turn non-nullable fields into nullable ones. (See [“Client-Controlled Nullability” in Graphcache for an example of a client implementing this.](https://formidable.com/open-source/urql/docs/graphcache/local-directives/#client-controlled-nullability))
23 changes: 23 additions & 0 deletions src/__tests__/selection.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,29 @@ test('infers optional properties for @skip/@include', () => {
expectTypeOf<expected>().toEqualTypeOf<actual>();
});

test('infers nullable field types for @required/@optional', () => {
type query = parseDocument</* GraphQL */ `
query {
todos {
id @optional
complete @required
}
}
`>;

type actual = getDocumentType<query, schema>;
type expected = {
todos:
| ({
id: string | number | null;
complete: boolean;
} | null)[]
| null;
};

expectTypeOf<expected>().toEqualTypeOf<actual>();
});

test('infers optional fragment for @defer', () => {
type query = parseDocument</* GraphQL */ `
query {
Expand Down
26 changes: 22 additions & 4 deletions src/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,25 @@ type unwrapType<
SelectionSet extends { kind: Kind.SELECTION_SET } | undefined,
Introspection extends IntrospectionLikeType,
Fragments extends { [name: string]: any },
TypeDirective = void,
> = Type extends IntrospectionNonNullTypeRef
? _unwrapTypeRec<Type['ofType'], SelectionSet, Introspection, Fragments>
: null | _unwrapTypeRec<Type, SelectionSet, Introspection, Fragments>;
? TypeDirective extends 'optional'
? null | _unwrapTypeRec<Type['ofType'], SelectionSet, Introspection, Fragments>
: _unwrapTypeRec<Type['ofType'], SelectionSet, Introspection, Fragments>
: TypeDirective extends 'required'
? _unwrapTypeRec<Type, SelectionSet, Introspection, Fragments>
: null | _unwrapTypeRec<Type, SelectionSet, Introspection, Fragments>;

type getTypeDirective<Directives extends readonly unknown[] | undefined> =
Directives extends readonly [infer Directive, ...infer Rest]
? Directive extends { kind: Kind.DIRECTIVE; name: any }
? Directive['name']['value'] extends 'required' | '_required'
? 'required'
: Directive['name']['value'] extends 'optional' | '_optional'
? 'optional'
: getTypeDirective<Rest>
: getTypeDirective<Rest>
: void;

type isOptionalRec<Directives extends readonly unknown[] | undefined> =
Directives extends readonly [infer Directive, ...infer Rest]
Expand Down Expand Up @@ -146,7 +162,8 @@ type _getPossibleTypeSelectionRec<
Type['fields'][Node['name']['value']]['type'],
Node['selectionSet'],
Introspection,
Fragments
Fragments,
getTypeDirective<Node['directives']>
>;
}
: {
Expand All @@ -156,7 +173,8 @@ type _getPossibleTypeSelectionRec<
Type['fields'][Node['name']['value']]['type'],
Node['selectionSet'],
Introspection,
Fragments
Fragments,
getTypeDirective<Node['directives']>
>;
}
: {}) &
Expand Down

0 comments on commit d29ed37

Please sign in to comment.