Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flow support - spread operator #2550

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions packages/plugins/flow/flow/src/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,39 @@ export class FlowVisitor extends BaseTypesVisitor<FlowPluginConfig, FlowPluginPa
});
}

getObjectTypeDeclarationBlock(node: ObjectTypeDefinitionNode, originalNode: ObjectTypeDefinitionNode): DeclarationBlock {
const optionalTypename = this.config.nonOptionalTypename ? '__typename' : '__typename?';
const { type } = this._parsedConfig.declarationKind;
const allFields = [...(this.config.addTypename ? [indent(`${this.config['immutableTypes'] ? 'readonly' : ''} ${optionalTypename}: '${node.name}'${type === 'class' ? ';' : ','}`)] : []), ...node.fields];

const buildInterfaces = () => {
if (!originalNode.interfaces || !node.interfaces.length) {
return '';
}

const interfaces = originalNode.interfaces.map(i => this.convertName(i));

if (type === 'interface' || type === 'class') {
return ' extends ' + interfaces.join(', ') + (allFields.length ? ' ' : ' {}');
}

// return interfaces.join(' & ') + (allFields.length ? ' & ' : '');

return '{' + '...' + interfaces.join(', ...') + (allFields.length ? ', ...' : '}');
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think to change it in a base behind isFlow flag. Will be less changes

};
const interfaces = buildInterfaces();

let declarationBlock = new DeclarationBlock(this._declarationBlockConfig)
.export()
.withFlow()
.asKind(type)
.withName(this.convertName(node))
.withContent(interfaces)
.withComment((node.description as any) as string);

return declarationBlock.withBlock(allFields.join('\n'));
}

protected _getScalar(name: string): string {
return `$ElementType<Scalars, '${name}'>`;
}
Expand Down Expand Up @@ -93,6 +126,7 @@ export class FlowVisitor extends BaseTypesVisitor<FlowPluginConfig, FlowPluginPa

const enumValues = new DeclarationBlock(this._declarationBlockConfig)
.export()
.withFlow()
.asKind('const')
.withName(enumValuesName)
.withMethodCall('Object.freeze', true)
Expand All @@ -114,6 +148,7 @@ export class FlowVisitor extends BaseTypesVisitor<FlowPluginConfig, FlowPluginPa

const enumType = new DeclarationBlock(this._declarationBlockConfig)
.export()
.withFlow()
.asKind('type')
.withName(this.convertName(node, { useTypesPrefix: this.config.enumPrefix }))
.withComment((node.description as any) as string)
Expand Down
44 changes: 22 additions & 22 deletions packages/plugins/flow/flow/tests/flow.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,20 +482,20 @@ describe('Flow Plugin', () => {
id: $ElementType<Scalars, 'ID'>,
};`);

expect(result.content).toBeSimilarStringTo(`export type impl1 = some_interface & {
expect(result.content).toBeSimilarStringTo(`export type impl1 = {...some_interface, ...{
__typename?: 'Impl1',
id: $ElementType<Scalars, 'ID'>,
};`);
}};`);

expect(result.content).toBeSimilarStringTo(`export type impl_2 = some_interface & {
expect(result.content).toBeSimilarStringTo(`export type impl_2 = {...some_interface, ...{
__typename?: 'Impl_2',
id: $ElementType<Scalars, 'ID'>,
};`);
}};`);

expect(result.content).toBeSimilarStringTo(`export type impl_3 = some_interface & {
expect(result.content).toBeSimilarStringTo(`export type impl_3 = {...some_interface, ...{
__typename?: 'impl_3',
id: $ElementType<Scalars, 'ID'>,
};`);
}};`);

expect(result.content).toBeSimilarStringTo(`export type query = {
__typename?: 'Query',
Expand Down Expand Up @@ -537,20 +537,20 @@ describe('Flow Plugin', () => {
id: $ElementType<Scalars, 'ID'>,
};`);

expect(result.content).toBeSimilarStringTo(`export type Impl1 = Some_Interface & {
expect(result.content).toBeSimilarStringTo(`export type Impl1 = {...Some_Interface, ...{
__typename?: 'Impl1',
id: $ElementType<Scalars, 'ID'>,
};`);
}};`);

expect(result.content).toBeSimilarStringTo(`export type Impl_2 = Some_Interface & {
expect(result.content).toBeSimilarStringTo(`export type Impl_2 = {...Some_Interface, ...{
__typename?: 'Impl_2',
id: $ElementType<Scalars, 'ID'>,
};`);
}};`);

expect(result.content).toBeSimilarStringTo(`export type Impl_3 = Some_Interface & {
expect(result.content).toBeSimilarStringTo(`export type Impl_3 = {...Some_Interface, ...{
__typename?: 'impl_3',
id: $ElementType<Scalars, 'ID'>,
};`);
}};`);

expect(result.content).toBeSimilarStringTo(`export type Query = {
__typename?: 'Query',
Expand Down Expand Up @@ -591,20 +591,20 @@ describe('Flow Plugin', () => {
id: $ElementType<Scalars, 'ID'>,
};`);

expect(result.content).toBeSimilarStringTo(`export type IImpl1 = ISome_Interface & {
expect(result.content).toBeSimilarStringTo(`export type IImpl1 = {...ISome_Interface, ...{
__typename?: 'Impl1',
id: $ElementType<Scalars, 'ID'>,
};`);
}};`);

expect(result.content).toBeSimilarStringTo(`export type IImpl_2 = ISome_Interface & {
expect(result.content).toBeSimilarStringTo(`export type IImpl_2 = {...ISome_Interface, ...{
__typename?: 'Impl_2',
id: $ElementType<Scalars, 'ID'>,
};`);
}};`);

expect(result.content).toBeSimilarStringTo(`export type IImpl_3 = ISome_Interface & {
expect(result.content).toBeSimilarStringTo(`export type IImpl_3 = {...ISome_Interface, ...{
__typename?: 'impl_3',
id: $ElementType<Scalars, 'ID'>,
};`);
}};`);

expect(result.content).toBeSimilarStringTo(`export type IQuery = {
__typename?: 'Query',
Expand Down Expand Up @@ -869,10 +869,10 @@ describe('Flow Plugin', () => {
};
`);
expect(result.content).toBeSimilarStringTo(`
export type MyType = MyInterface & {
export type MyType = {...MyInterface, ...{
__typename?: 'MyType',
foo: $ElementType<Scalars, 'String'>,
};
}};
`);
validateFlow(result);
});
Expand Down Expand Up @@ -905,11 +905,11 @@ describe('Flow Plugin', () => {
};
`);
expect(result.content).toBeSimilarStringTo(`
export type MyType = MyInterface & MyOtherInterface & {
export type MyType = {...MyInterface, ...MyOtherInterface, ...{
__typename?: 'MyType',
foo: $ElementType<Scalars, 'String'>,
bar: $ElementType<Scalars, 'String'>,
};
}};
`);
validateFlow(result);
});
Expand Down
182 changes: 180 additions & 2 deletions packages/plugins/flow/operations/src/flow-selection-set-to-object.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,66 @@
import { SelectionSetToObject, PrimitiveField, PrimitiveAliasedFields, LinkField, ConvertNameFn, NormalizedScalarsMap, LoadedFragment } from '@graphql-codegen/visitor-plugin-common';
import { GraphQLObjectType, GraphQLNonNull, GraphQLList, isNonNullType, isListType, GraphQLSchema, GraphQLNamedType, SelectionSetNode } from 'graphql';
import { ConvertNameFn, getBaseType, LinkField, LoadedFragment, NormalizedScalarsMap, PrimitiveAliasedFields, PrimitiveField, SelectionSetToObject } from '@graphql-codegen/visitor-plugin-common';
import {
FieldNode,
FragmentSpreadNode,
GraphQLField,
GraphQLList,
GraphQLNamedType,
GraphQLNonNull,
GraphQLObjectType,
GraphQLOutputType,
GraphQLSchema,
isListType,
isNonNullType,
SchemaMetaFieldDef,
SelectionNode,
SelectionSetNode,
TypeMetaFieldDef,
} from 'graphql';
import { FlowDocumentsParsedConfig } from './visitor';

const getFieldNodeNameValue = (node: FieldNode): string => {
return (node.alias || node.name).value;
};

const metadataFieldMap: Record<string, GraphQLField<any, any>> = {
__schema: SchemaMetaFieldDef,
__type: TypeMetaFieldDef,
};

const mergeSelectionSets = (selectionSet1: SelectionSetNode, selectionSet2: SelectionSetNode) => {
const newSelections = [...selectionSet1.selections];

for (const selection2 of selectionSet2.selections) {
if (selection2.kind === 'FragmentSpread') {
newSelections.push(selection2);
continue;
}

if (selection2.kind !== 'Field') {
throw new TypeError('Invalid state.');
}

const match = newSelections.find(selection1 => selection1.kind === 'Field' && getFieldNodeNameValue(selection1) === getFieldNodeNameValue(selection2));

if (match) {
// recursively merge all selection sets
if (match.kind === 'Field' && match.selectionSet && selection2.selectionSet) {
mergeSelectionSets(match.selectionSet, selection2.selectionSet);
}
continue;
}

newSelections.push(selection2);
}

// replace existing selections
selectionSet1.selections = newSelections;
};

function isMetadataFieldName(name: string) {
return ['__schema', '__type'].includes(name);
}

export class FlowSelectionSetToObject extends SelectionSetToObject {
constructor(
_scalars: NormalizedScalarsMap,
Expand Down Expand Up @@ -31,6 +90,125 @@ export class FlowSelectionSetToObject extends SelectionSetToObject {
);
}

protected buildSelectionSetString(parentSchemaType: GraphQLObjectType, selectionNodes: SelectionNode[]) {
const primitiveFields = new Map<string, FieldNode>();
const primitiveAliasFields = new Map<string, FieldNode>();
const linkFieldSelectionSets = new Map<
string,
{
selectedFieldType: GraphQLOutputType;
field: FieldNode;
}
>();
const fragmentSpreadSelectionSets = new Map<string, FragmentSpreadNode>();
let requireTypename = false;

for (const selectionNode of selectionNodes) {
if (selectionNode.kind === 'Field') {
if (!selectionNode.selectionSet) {
if (selectionNode.alias) {
primitiveAliasFields.set(selectionNode.alias.value, selectionNode);
} else if (selectionNode.name.value === '__typename') {
requireTypename = true;
} else {
primitiveFields.set(selectionNode.name.value, selectionNode);
}
} else {
let selectedField: GraphQLField<any, any, any> = null;

const fields = parentSchemaType.getFields();
selectedField = fields[selectionNode.name.value];

if (isMetadataFieldName(selectionNode.name.value)) {
selectedField = metadataFieldMap[selectionNode.name.value];
}

if (!selectedField) {
throw new TypeError(`Could not find field type. ${parentSchemaType}.${selectionNode.name.value}`);
}

const fieldName = getFieldNodeNameValue(selectionNode);
let linkFieldNode = linkFieldSelectionSets.get(fieldName);
if (!linkFieldNode) {
linkFieldNode = {
selectedFieldType: selectedField.type,
field: selectionNode,
};
linkFieldSelectionSets.set(fieldName, linkFieldNode);
} else {
mergeSelectionSets(linkFieldNode.field.selectionSet, selectionNode.selectionSet);
}
}
} else if (selectionNode.kind === 'FragmentSpread') {
fragmentSpreadSelectionSets.set(selectionNode.name.value, selectionNode);
}
}

const linkFields: LinkField[] = [];
for (const { field, selectedFieldType } of linkFieldSelectionSets.values()) {
const realSelectedFieldType = getBaseType(selectedFieldType as any);
const selectionSet = this.createNext(realSelectedFieldType, field.selectionSet);

linkFields.push({
alias: field.alias ? field.alias.value : undefined,
name: field.name.value,
type: realSelectedFieldType.name,
selectionSet: this.wrapTypeWithModifiers(selectionSet.string.split(`\n`).join(`\n `), selectedFieldType as any),
});
}

const parentName =
(this._namespacedImportName ? `${this._namespacedImportName}.` : '') +
this._convertName(parentSchemaType.name, {
useTypesPrefix: true,
});

const typeInfoField = this.buildTypeNameField(parentSchemaType, this._nonOptionalTypename, this._addTypename, requireTypename);

if (this._preResolveTypes) {
const primitiveFieldsTypes = this.buildPrimitiveFieldsWithoutPick(parentSchemaType, Array.from(primitiveFields.values()).map(field => field.name.value));
const primitiveAliasTypes = this.buildAliasedPrimitiveFieldsWithoutPick(
parentSchemaType,
Array.from(primitiveAliasFields.values()).map(field => ({
alias: field.alias.value,
fieldName: field.name.value,
}))
);
const linkFieldsTypes = this.buildLinkFieldsWithoutPick(linkFields);

return `{ ${[typeInfoField, ...primitiveFieldsTypes, ...primitiveAliasTypes, ...linkFieldsTypes]
.filter(a => a)
.map(b => `${b.name}: ${b.type}`)
.join(', ')} }`;
}

let typeInfoString: null | string = null;
if (typeInfoField) {
typeInfoString = `{ ${typeInfoField.name}: ${typeInfoField.type} }`;
}

const primitiveFieldsString = this.buildPrimitiveFields(parentName, Array.from(primitiveFields.values()).map(field => field.name.value));
const primitiveAliasFieldsString = this.buildAliasedPrimitiveFields(
parentName,
Array.from(primitiveAliasFields.values()).map(field => ({
alias: field.alias.value,
fieldName: field.name.value,
}))
);
const linkFieldsString = this.buildLinkFields(linkFields);
const fragmentSpreadString = this.buildFragmentSpreadString([...fragmentSpreadSelectionSets.values()]);

const result = [typeInfoString, primitiveFieldsString, primitiveAliasFieldsString, linkFieldsString, fragmentSpreadString].filter(Boolean);

if (result.length === 0) {
return null;
} else if (result.length === 1) {
return result[0];
} else {
return `{\n ...` + result.join(`,\n ...`) + `\n}`;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think to change it in a base behind isFlow flag. Will be less changes

}
}

public createNext(parentSchemaType: GraphQLNamedType, selectionSet: SelectionSetNode): SelectionSetToObject {
return new FlowSelectionSetToObject(this._scalars, this._schema, this._convertName, this._addTypename, this._preResolveTypes, this._nonOptionalTypename, this._loadedFragments, this._visitorConfig, parentSchemaType, selectionSet);
}
Expand Down
Loading