From a3ea4cae34349c0489d55674975d54c9bf8875f8 Mon Sep 17 00:00:00 2001 From: matikrk Date: Fri, 13 Sep 2019 15:46:22 +0200 Subject: [PATCH 1/2] Better flow support - tests --- packages/plugins/flow/flow/tests/flow.spec.ts | 44 ++-- .../operations/tests/flow-documents.spec.ts | 220 +++++++++--------- 2 files changed, 132 insertions(+), 132 deletions(-) diff --git a/packages/plugins/flow/flow/tests/flow.spec.ts b/packages/plugins/flow/flow/tests/flow.spec.ts index 0dc2a39a891..543c888e206 100644 --- a/packages/plugins/flow/flow/tests/flow.spec.ts +++ b/packages/plugins/flow/flow/tests/flow.spec.ts @@ -482,20 +482,20 @@ describe('Flow Plugin', () => { id: $ElementType, };`); - expect(result.content).toBeSimilarStringTo(`export type impl1 = some_interface & { + expect(result.content).toBeSimilarStringTo(`export type impl1 = {...some_interface, ...{ __typename?: 'Impl1', id: $ElementType, - };`); + }};`); - 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, - };`); + }};`); - 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, - };`); + }};`); expect(result.content).toBeSimilarStringTo(`export type query = { __typename?: 'Query', @@ -537,20 +537,20 @@ describe('Flow Plugin', () => { id: $ElementType, };`); - expect(result.content).toBeSimilarStringTo(`export type Impl1 = Some_Interface & { + expect(result.content).toBeSimilarStringTo(`export type Impl1 = {...Some_Interface, ...{ __typename?: 'Impl1', id: $ElementType, - };`); + }};`); - 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, - };`); + }};`); - 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, - };`); + }};`); expect(result.content).toBeSimilarStringTo(`export type Query = { __typename?: 'Query', @@ -591,20 +591,20 @@ describe('Flow Plugin', () => { id: $ElementType, };`); - expect(result.content).toBeSimilarStringTo(`export type IImpl1 = ISome_Interface & { + expect(result.content).toBeSimilarStringTo(`export type IImpl1 = {...ISome_Interface, ...{ __typename?: 'Impl1', id: $ElementType, - };`); + }};`); - 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, - };`); + }};`); - 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, - };`); + }};`); expect(result.content).toBeSimilarStringTo(`export type IQuery = { __typename?: 'Query', @@ -869,10 +869,10 @@ describe('Flow Plugin', () => { }; `); expect(result.content).toBeSimilarStringTo(` - export type MyType = MyInterface & { + export type MyType = {...MyInterface, ...{ __typename?: 'MyType', foo: $ElementType, - }; + }}; `); validateFlow(result); }); @@ -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, bar: $ElementType, - }; + }}; `); validateFlow(result); }); diff --git a/packages/plugins/flow/operations/tests/flow-documents.spec.ts b/packages/plugins/flow/operations/tests/flow-documents.spec.ts index 7f4ba0223d9..e9007dda090 100644 --- a/packages/plugins/flow/operations/tests/flow-documents.spec.ts +++ b/packages/plugins/flow/operations/tests/flow-documents.spec.ts @@ -106,20 +106,20 @@ describe('Flow Operations Plugin', () => { ); expect(result).toBeSimilarStringTo(` - export type notificationsquery = ( - { __typename?: 'Query' } - & { notifications: Array<( - { __typename?: 'TextNotification' } - & $Pick - ) | ( - { __typename?: 'ImageNotification' } - & $Pick - & { metadata: ( - { __typename?: 'ImageMetadata' } - & $Pick - ) } - )> } - ); + export type notificationsquery = { + ...{ __typename?: 'Query' }, + ...{ notifications: Array<{ + ...{ __typename?: 'TextNotification' }, + ...$Pick + } | { + ...{ __typename?: 'ImageNotification' }, + ...$Pick, + ...{ metadata: { + ...{ __typename?: 'ImageMetadata' }, + ...$Pick + } } + }> } + }; `); validateFlow(result); }); @@ -157,20 +157,20 @@ describe('Flow Operations Plugin', () => { expect(result).toBeSimilarStringTo(`export type inotificationsqueryvariables = {};`); expect(result).toBeSimilarStringTo(` - export type inotificationsquery = ( - { __typename?: 'Query' } - & { notifications: Array<( - { __typename?: 'TextNotification' } - & $Pick - ) | ( - { __typename?: 'ImageNotification' } - & $Pick - & { metadata: ( - { __typename?: 'ImageMetadata' } - & $Pick - ) } - )> } - ); + export type inotificationsquery = { + ...{ __typename?: 'Query' }, + ...{ notifications: Array<{ + ...{ __typename?: 'TextNotification' }, + ...$Pick + } | { + ...{ __typename?: 'ImageNotification' }, + ...$Pick, + ...{ metadata: { + ...{ __typename?: 'ImageMetadata' }, + ...$Pick + } } + }> } + }; `); validateFlow(result); }); @@ -217,10 +217,10 @@ describe('Flow Operations Plugin', () => { { outputFile: '' } ); expect(result).toBeSimilarStringTo(` - export type Unnamed_1_Query = ( - { __typename: 'Query' } - & $Pick - ); + export type Unnamed_1_Query = { + ...{ __typename: 'Query' }, + ...$Pick + }; `); validateFlow(result); }); @@ -243,10 +243,10 @@ describe('Flow Operations Plugin', () => { { outputFile: '' } ); expect(result).toBeSimilarStringTo(` - export type Unnamed_1_Query = ( - { __typename?: 'Query' } - & $Pick - ); + export type Unnamed_1_Query = { + ...{ __typename?: 'Query' }, + ...$Pick + }; `); validateFlow(result); }); @@ -270,10 +270,10 @@ describe('Flow Operations Plugin', () => { { outputFile: '' } ); expect(result).toBeSimilarStringTo(` - export type Unnamed_1_Query = ( - { __typename: 'Query' } - & $Pick - ); + export type Unnamed_1_Query = { + ...{ __typename: 'Query' }, + ...$Pick + }; `); validateFlow(result); }); @@ -305,16 +305,16 @@ describe('Flow Operations Plugin', () => { ); expect(result).toBeSimilarStringTo(` - export type UnionTestQuery = ( - { __typename?: 'Query' } - & { unionTest: ?( - { __typename?: 'User' } - & $Pick - ) | ( - { __typename?: 'Profile' } - & $Pick - ) } - ); + export type UnionTestQuery = { + ...{ __typename?: 'Query' }, + ...{ unionTest: ?{ + ...{ __typename?: 'User' }, + ...$Pick + } | { + ...{ __typename?: 'Profile' }, + ...$Pick + } } + }; `); validateFlow(result); }); @@ -346,16 +346,16 @@ describe('Flow Operations Plugin', () => { ); expect(result).toBeSimilarStringTo(` - export type UnionTestQuery = ( - { __typename: 'Query' } - & { unionTest: ?( - { __typename: 'User' } - & $Pick - ) | ( - { __typename: 'Profile' } - & $Pick - ) } - ); + export type UnionTestQuery = { + ...{ __typename: 'Query' }, + ...{ unionTest: ?{ + ...{ __typename: 'User' }, + ...$Pick + } | { + ...{ __typename: 'Profile' }, + ...$Pick + } } + }; `); validateFlow(result); }); @@ -391,20 +391,20 @@ describe('Flow Operations Plugin', () => { { outputFile: '' } ); expect(result).toBeSimilarStringTo(` - export type NotificationsQuery = ( - { __typename?: 'Query' } - & { notifications: Array<( - { __typename?: 'TextNotification' } - & $Pick - ) | ( - { __typename?: 'ImageNotification' } - & $Pick - & { metadata: ( - { __typename?: 'ImageMetadata' } - & $Pick - ) } - )> } - ); + export type NotificationsQuery = { + ...{ __typename?: 'Query' }, + ...{ notifications: Array<{ + ...{ __typename?: 'TextNotification' }, + ...$Pick + } | { + ...{ __typename?: 'ImageNotification' }, + ...$Pick, + ...{ metadata: { + ...{ __typename?: 'ImageMetadata' }, + ...$Pick + } } + }> } + }; `); validateFlow(result); }); @@ -609,10 +609,10 @@ describe('Flow Operations Plugin', () => { ); expect(result).toBeSimilarStringTo(` - export type NotificationsQuery = { notifications: Array<$Pick | ( - $Pick - & { metadata: $Pick } - )> }; + export type NotificationsQuery = { notifications: Array<$Pick | { + ...$Pick, + ...{ metadata: $Pick } + }> }; `); validateFlow(result); }); @@ -676,10 +676,10 @@ describe('Flow Operations Plugin', () => { ); expect(result).toBeSimilarStringTo(` - export type CurrentUserQuery = { me: ?( - $Pick - & { profile: ?$Pick } - ) }; + export type CurrentUserQuery = { me: ?{ + ...$Pick, + ...{ profile: ?$Pick } + } }; `); validateFlow(result); }); @@ -718,10 +718,10 @@ describe('Flow Operations Plugin', () => { };` ); expect(result).toBeSimilarStringTo(` - export type MeQuery = { currentUser: ?$Pick, entry: ?( - $Pick - & { postedBy: $Pick } - ) }; + export type MeQuery = { currentUser: ?$Pick, entry: ?{ + ...$Pick, + ...{ postedBy: $Pick } + } }; `); validateFlow(result); }); @@ -769,10 +769,10 @@ describe('Flow Operations Plugin', () => { ); expect(result).toBeSimilarStringTo(` - export type DummyQuery = ( - { customName: $ElementType } - & { customName2: ?$Pick } - ); + export type DummyQuery = { + ...{ customName: $ElementType }, + ...{ customName2: ?$Pick } + }; `); validateFlow(result); }); @@ -803,10 +803,10 @@ describe('Flow Operations Plugin', () => { ); expect(result).toBeSimilarStringTo(` - export type CurrentUserQuery = { me: ?( - $Pick - & { profile: ?$Pick } - ) }; + export type CurrentUserQuery = { me: ?{ + ...$Pick, + ...{ profile: ?$Pick } + } }; `); validateFlow(result); @@ -837,10 +837,10 @@ describe('Flow Operations Plugin', () => { ); expect(result).toBeSimilarStringTo(` - export type UserFieldsFragment = ( - $Pick - & { profile: ?$Pick } - ); + export type UserFieldsFragment = { + ...$Pick, + ...{ profile: ?$Pick } + }; `); validateFlow(result); }); @@ -872,10 +872,10 @@ describe('Flow Operations Plugin', () => { ); expect(result).toBeSimilarStringTo(` - export type LoginMutation = { login: ?( - $Pick - & { profile: ?$Pick } - ) }; + export type LoginMutation = { login: ?{ + ...$Pick, + ...{ profile: ?$Pick } + } }; `); validateFlow(result); }); @@ -1008,10 +1008,10 @@ describe('Flow Operations Plugin', () => { ); expect(result).toBeSimilarStringTo(` - export type CurrentUserQuery = {| me: ?( - $Pick - & {| profile: ?$Pick |} - ) |}; + export type CurrentUserQuery = {| me: ?{ + ...$Pick, + ...{| profile: ?$Pick |} + } |}; `); validateFlow(result); @@ -1042,10 +1042,10 @@ describe('Flow Operations Plugin', () => { { outputFile: '' } ); expect(result).toBeSimilarStringTo(` - export type CurrentUserQuery = { +me: ?( - $Pick - & { +profile: ?$Pick } - ) }; + export type CurrentUserQuery = { +me: ?{ + ...$Pick, + ...{ +profile: ?$Pick } + } }; `); validateFlow(result); From 4577d25c9942161f7bb1a1007d7ebbe339a451da Mon Sep 17 00:00:00 2001 From: matikrk Date: Fri, 13 Sep 2019 15:47:45 +0200 Subject: [PATCH 2/2] Better flow support - code --- packages/plugins/flow/flow/src/visitor.ts | 35 ++++ .../src/flow-selection-set-to-object.ts | 182 +++++++++++++++++- .../plugins/flow/resolvers/src/visitor.ts | 1 + .../other/visitor-plugin-common/src/utils.ts | 11 ++ 4 files changed, 227 insertions(+), 2 deletions(-) diff --git a/packages/plugins/flow/flow/src/visitor.ts b/packages/plugins/flow/flow/src/visitor.ts index 718fedf40e4..4c0edd572a1 100644 --- a/packages/plugins/flow/flow/src/visitor.ts +++ b/packages/plugins/flow/flow/src/visitor.ts @@ -26,6 +26,39 @@ export class FlowVisitor extends BaseTypesVisitor { + 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 ? ', ...' : '}'); + }; + 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`; } @@ -93,6 +126,7 @@ export class FlowVisitor extends BaseTypesVisitor { + return (node.alias || node.name).value; +}; + +const metadataFieldMap: Record> = { + __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, @@ -31,6 +90,125 @@ export class FlowSelectionSetToObject extends SelectionSetToObject { ); } + protected buildSelectionSetString(parentSchemaType: GraphQLObjectType, selectionNodes: SelectionNode[]) { + const primitiveFields = new Map(); + const primitiveAliasFields = new Map(); + const linkFieldSelectionSets = new Map< + string, + { + selectedFieldType: GraphQLOutputType; + field: FieldNode; + } + >(); + const fragmentSpreadSelectionSets = new Map(); + 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 = 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}`; + } + } + 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); } diff --git a/packages/plugins/flow/resolvers/src/visitor.ts b/packages/plugins/flow/resolvers/src/visitor.ts index 6ffaab85249..7468b320934 100644 --- a/packages/plugins/flow/resolvers/src/visitor.ts +++ b/packages/plugins/flow/resolvers/src/visitor.ts @@ -97,6 +97,7 @@ export class FlowResolversVisitor extends BaseResolversVisitor