Skip to content

Commit

Permalink
Add initial typescript @defer support
Browse files Browse the repository at this point in the history
This commit takes a first pass at introducing `@defer` typescript codegen
support. It follows a similar approach as the `@skip` / `@include`
functionality introduced in dotansimha#5017.

Related issue: dotansimha#7885
  • Loading branch information
hwillson committed Jan 22, 2023
1 parent 22e723a commit e1ea93d
Show file tree
Hide file tree
Showing 9 changed files with 650 additions and 25 deletions.
6 changes: 6 additions & 0 deletions .changeset/calm-oranges-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@graphql-codegen/visitor-plugin-common': minor
'@graphql-codegen/typescript-operations': minor
---

Add typescript codegen `@defer` support
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { GraphQLInterfaceType, GraphQLNamedType, GraphQLObjectType, GraphQLOutputType } from 'graphql';
import { AvoidOptionalsConfig, ConvertNameFn, ScalarsMap } from '../types.js';

export type PrimitiveField = { isConditional: boolean; fieldName: string };
export type PrimitiveField = { isConditional: boolean; isIncremental: boolean; fieldName: string };
export type PrimitiveAliasedFields = { alias: string; fieldName: string };
export type LinkField = { alias: string; name: string; type: string; selectionSet: string };
export type NameAndType = { name: string; type: string };
Expand All @@ -12,7 +12,12 @@ export type SelectionSetProcessorConfig = {
convertName: ConvertNameFn<any>;
enumPrefix: boolean | null;
scalars: ScalarsMap;
formatNamedField(name: string, type?: GraphQLOutputType | GraphQLNamedType | null, isConditional?: boolean): string;
formatNamedField(
name: string,
type?: GraphQLOutputType | GraphQLNamedType | null,
isConditional?: boolean,
isIncremental?: boolean
): string;
wrapTypeWithModifiers(baseType: string, type: GraphQLOutputType | GraphQLNamedType): string;
avoidOptionals?: AvoidOptionalsConfig;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class PreResolveTypesProcessor extends BaseSelectionSetProcessor<Selectio
const baseType = getBaseType(fieldObj.type);
let typeToUse = baseType.name;

const useInnerType = field.isConditional && isNonNullType(fieldObj.type);
const useInnerType = (field.isConditional || field.isIncremental) && isNonNullType(fieldObj.type);
const innerType = useInnerType ? removeNonNullWrapper(fieldObj.type) : undefined;

if (isEnumType(baseType)) {
Expand All @@ -47,7 +47,8 @@ export class PreResolveTypesProcessor extends BaseSelectionSetProcessor<Selectio
const name = this.config.formatNamedField(
field.fieldName,
useInnerType ? innerType : fieldObj.type,
field.isConditional
field.isConditional,
field.isIncremental
);
const wrappedType = this.config.wrapTypeWithModifiers(typeToUse, fieldObj.type);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,19 @@ import {
PrimitiveField,
ProcessResult,
} from './selection-set-processor/base.js';
import { ConvertNameFn, GetFragmentSuffixFn, LoadedFragment, NormalizedScalarsMap } from './types.js';
import {
ConvertNameFn,
GetFragmentSuffixFn,
LoadedFragment,
NormalizedScalarsMap,
FragmentDirectives
} from './types.js';
import {
DeclarationBlock,
getFieldNodeNameValue,
getPossibleTypes,
hasConditionalDirectives,
hasIncrementalDeliveryDirectives,
mergeSelectionSets,
separateSelectionSet,
} from './utils.js';
Expand All @@ -51,6 +58,8 @@ type FragmentSpreadUsage = {
selectionNodes: Array<SelectionNode>;
};

type CollectedFragmentNode = (SelectionNode | FragmentSpreadUsage | DirectiveNode) & FragmentDirectives;

function isMetadataFieldName(name: string) {
return ['__schema', '__type'].includes(name);
}
Expand Down Expand Up @@ -99,8 +108,8 @@ export class SelectionSetToObject<Config extends ParsedDocumentsConfig = ParsedD
*/
_collectInlineFragments(
parentType: GraphQLNamedType,
nodes: InlineFragmentNode[],
types: Map<string, Array<SelectionNode | FragmentSpreadUsage | DirectiveNode>>
nodes: (InlineFragmentNode & FragmentDirectives)[],
types: Map<string, Array<CollectedFragmentNode>>
) {
if (isListType(parentType) || isNonNullType(parentType)) {
return this._collectInlineFragments(parentType.ofType as GraphQLNamedType, nodes, types);
Expand All @@ -112,8 +121,18 @@ export class SelectionSetToObject<Config extends ParsedDocumentsConfig = ParsedD
const spreadsUsage = this.buildFragmentSpreadsUsage(spreads);
const directives = (node.directives as DirectiveNode[]) || undefined;

// When we collect the selection sets of inline fragments we need to
// make sure directives on the inline fragments are stored in a way
// that can be associated back to the fields in the fragment, to
// support things like making those fields optional when deferring a
// fragment (using @defer).
const fieldsWithFragmentDirectives: CollectedFragmentNode[] = fields.map(field => ({
...field,
fragmentDirectives: field.fragmentDirectives || directives,
}));

if (isObjectType(typeOnSchema)) {
this._appendToTypeMap(types, typeOnSchema.name, fields);
this._appendToTypeMap(types, typeOnSchema.name, fieldsWithFragmentDirectives);
this._appendToTypeMap(types, typeOnSchema.name, spreadsUsage[typeOnSchema.name]);
this._appendToTypeMap(types, typeOnSchema.name, directives);
this._collectInlineFragments(typeOnSchema, inlines, types);
Expand Down Expand Up @@ -232,11 +251,18 @@ export class SelectionSetToObject<Config extends ParsedDocumentsConfig = ParsedD

selectionNodesByTypeName[possibleType.name] ||= [];

const selectionNodes = fragmentSpreadObject.node.selectionSet.selections.map(selectionNode => {
return {
...selectionNode,
fragmentDirectives: [...(spread.directives || [])],
};
});

selectionNodesByTypeName[possibleType.name].push({
fragmentName: spread.name.value,
typeName: usage,
onType: fragmentSpreadObject.onType,
selectionNodes: [...fragmentSpreadObject.node.selectionSet.selections],
selectionNodes,
});
}
}
Expand All @@ -253,7 +279,6 @@ export class SelectionSetToObject<Config extends ParsedDocumentsConfig = ParsedD
const inlineFragmentSelections: InlineFragmentNode[] = [];
const fieldNodes: FieldNode[] = [];
const fragmentSpreads: FragmentSpreadNode[] = [];

for (const selection of selections) {
switch (selection.kind) {
case Kind.FIELD:
Expand Down Expand Up @@ -288,7 +313,11 @@ export class SelectionSetToObject<Config extends ParsedDocumentsConfig = ParsedD
return selectionNodesByTypeName;
}

private _appendToTypeMap<T = SelectionNode>(types: Map<string, Array<T>>, typeName: string, nodes: Array<T>): void {
private _appendToTypeMap<T = CollectedFragmentNode>(
types: Map<string, Array<T>>,
typeName: string,
nodes: Array<T>
): void {
if (!types.has(typeName)) {
types.set(typeName, []);
}
Expand Down Expand Up @@ -442,7 +471,6 @@ export class SelectionSetToObject<Config extends ParsedDocumentsConfig = ParsedD
// ensure we mutate no function params
selectionNodes = [...selectionNodes];
let inlineFragmentConditional = false;

for (const selectionNode of selectionNodes) {
if ('kind' in selectionNode) {
if (selectionNode.kind === 'Field') {
Expand Down Expand Up @@ -531,9 +559,15 @@ export class SelectionSetToObject<Config extends ParsedDocumentsConfig = ParsedD
const realSelectedFieldType = getBaseType(selectedFieldType as any);
const selectionSet = this.createNext(realSelectedFieldType, field.selectionSet);
const isConditional = hasConditionalDirectives(field) || inlineFragmentConditional;
const isIncremental = hasIncrementalDeliveryDirectives(field);
linkFields.push({
alias: field.alias ? this._processor.config.formatNamedField(field.alias.value, selectedFieldType) : undefined,
name: this._processor.config.formatNamedField(field.name.value, selectedFieldType, isConditional),
name: this._processor.config.formatNamedField(
field.name.value,
selectedFieldType,
isConditional,
isIncremental
),
type: realSelectedFieldType.name,
selectionSet: this._processor.config.wrapTypeWithModifiers(
selectionSet.transformSelectionSet().split(`\n`).join(`\n `),
Expand All @@ -560,6 +594,7 @@ export class SelectionSetToObject<Config extends ParsedDocumentsConfig = ParsedD
parentSchemaType,
Array.from(primitiveFields.values()).map(field => ({
isConditional: hasConditionalDirectives(field),
isIncremental: hasIncrementalDeliveryDirectives(field),
fieldName: field.name.value,
}))
),
Expand Down
6 changes: 5 additions & 1 deletion packages/plugins/other/visitor-plugin-common/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ASTNode, FragmentDefinitionNode } from 'graphql';
import { ASTNode, FragmentDefinitionNode, DirectiveNode } from 'graphql';
import { ParsedMapper } from './mappers.js';

/**
Expand Down Expand Up @@ -102,3 +102,7 @@ export interface ParsedImport {
moduleName: string | null;
propName: string;
}

export type FragmentDirectives = {
fragmentDirectives?: Array<DirectiveNode>;
};
9 changes: 7 additions & 2 deletions packages/plugins/other/visitor-plugin-common/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
import { RawConfig } from './base-visitor.js';
import { parseMapper } from './mappers.js';
import { DEFAULT_SCALARS } from './scalars.js';
import { NormalizedScalarsMap, ParsedScalarsMap, ScalarsMap } from './types.js';
import { NormalizedScalarsMap, ParsedScalarsMap, ScalarsMap, FragmentDirectives } from './types.js';

export const getConfigValue = <T = any>(value: T, defaultValue: T): T => {
if (value === null || value === undefined) {
Expand Down Expand Up @@ -408,7 +408,7 @@ export const getFieldNodeNameValue = (node: FieldNode): string => {
};

export function separateSelectionSet(selections: ReadonlyArray<SelectionNode>): {
fields: FieldNode[];
fields: (FieldNode & FragmentDirectives)[];
spreads: FragmentSpreadNode[];
inlines: InlineFragmentNode[];
} {
Expand Down Expand Up @@ -438,6 +438,11 @@ export function hasConditionalDirectives(field: FieldNode): boolean {
return field.directives?.some(directive => CONDITIONAL_DIRECTIVES.includes(directive.name.value));
}

export function hasIncrementalDeliveryDirectives(field: FieldNode & FragmentDirectives): boolean {
const INCREMENTAL_DELIVERY_DIRECTIVES = ['defer'];
return field.fragmentDirectives?.some(directive => INCREMENTAL_DELIVERY_DIRECTIVES.includes(directive.name.value));
}

type WrapModifiersOptions = {
wrapOptional(type: string): string;
wrapArray(type: string): string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,19 @@ export class TypeScriptSelectionSetProcessor extends BaseSelectionSetProcessor<S
useTypesPrefix: true,
});

let hasConditionals = false;
const conditilnalsList: string[] = [];
let hasOptional = false;
const optionalList: string[] = [];
let resString = `Pick<${parentName}, ${fields
.map(field => {
if (field.isConditional) {
hasConditionals = true;
conditilnalsList.push(field.fieldName);
if (field.isConditional || field.isIncremental) {
hasOptional = true;
optionalList.push(field.fieldName);
}
return `'${field.fieldName}'`;
})
.join(' | ')}>`;

if (hasConditionals) {
if (hasOptional) {
const avoidOptional =
// TODO: check type and exec only if relevant
this.config.avoidOptionals === true ||
Expand All @@ -45,7 +45,7 @@ export class TypeScriptSelectionSetProcessor extends BaseSelectionSetProcessor<S
const transform = avoidOptional ? 'MakeMaybe' : 'MakeOptional';
resString = `${
this.config.namespacedImportName ? `${this.config.namespacedImportName}.` : ''
}${transform}<${resString}, ${conditilnalsList.map(field => `'${field}'`).join(' | ')}>`;
}${transform}<${resString}, ${optionalList.map(field => `'${field}'`).join(' | ')}>`;
}
return [resString];
}
Expand Down
8 changes: 6 additions & 2 deletions packages/plugins/typescript/operations/src/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,13 @@ export class TypeScriptDocumentsVisitor extends BaseDocumentsVisitor<
const formatNamedField = (
name: string,
type: GraphQLOutputType | GraphQLNamedType | null,
isConditional = false
isConditional = false,
isIncremental = false
): string => {
const optional = isConditional || (!this.config.avoidOptionals.field && Boolean(type) && !isNonNullType(type));
const optional =
isConditional ||
isIncremental ||
(!this.config.avoidOptionals.field && Boolean(type) && !isNonNullType(type));
return (this.config.immutableTypes ? `readonly ${name}` : name) + (optional ? '?' : '');
};

Expand Down
Loading

0 comments on commit e1ea93d

Please sign in to comment.