diff --git a/CHANGELOG.md b/CHANGELOG.md index 06ad0363..6fe5164f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # CHANGELOG +## 6.6.1-beta.1 +- allow multiple operations per file + ## 6.5.2-beta.1 - performance improvements - scan schema for canonical types only once diff --git a/lib/generator.dart b/lib/generator.dart index bebed397..5c3c3971 100644 --- a/lib/generator.dart +++ b/lib/generator.dart @@ -34,8 +34,8 @@ LibraryDefinition generateLibrary( List fragmentsCommon, DocumentNode schema, ) { - final queriesDefinitions = gqlDocs - .map((doc) => generateQuery( + final queryDefinitions = gqlDocs + .map((doc) => generateDefinitions( schema, path, doc, @@ -43,9 +43,12 @@ LibraryDefinition generateLibrary( schemaMap, fragmentsCommon, )) + .expand((e) => e) .toList(); - final allClassesNames = queriesDefinitions +// final queryDefinitions = definitions.expand((e) => e).toList(); + + final allClassesNames = queryDefinitions .map((def) => def.classes.map((c) => c)) .expand((e) => e) .toList(); @@ -63,7 +66,7 @@ LibraryDefinition generateLibrary( final customImports = _extractCustomImports(schema, options); return LibraryDefinition( basename: basename, - queries: queriesDefinitions, + queries: queryDefinitions, customImports: customImports, ); } @@ -100,7 +103,7 @@ _CanonicalVisitor _canonicalVisitor; /// Generate a query definition from a GraphQL schema and a query, given /// Artemis options and schema mappings. -QueryDefinition generateQuery( +Iterable generateDefinitions( DocumentNode schema, String path, DocumentNode document, @@ -108,9 +111,6 @@ QueryDefinition generateQuery( SchemaMap schemaMap, List fragmentsCommon, ) { - final operation = - document.definitions.whereType().first; - final fragments = []; final documentFragments = @@ -120,107 +120,117 @@ QueryDefinition generateQuery( throw FragmentIgnoreException(); } - if (fragmentsCommon.isEmpty) { - fragments.addAll(documentFragments); - } else { - final fragmentsOperation = - _extractFragments(operation.selectionSet, fragmentsCommon); - document.definitions.addAll(fragmentsOperation); - fragments.addAll(fragmentsOperation); - } - - final basename = p.basenameWithoutExtension(path).split('.').first; - final operationName = operation.name?.value ?? basename; - - final schemaVisitor = SchemaDefinitionVisitor(); - final objectVisitor = ObjectTypeDefinitionVisitor(); - - schema.accept(schemaVisitor); - schema.accept(objectVisitor); - - String suffix; - switch (operation.type) { - case OperationType.subscription: - suffix = 'Subscription'; - break; - case OperationType.mutation: - suffix = 'Mutation'; - break; - case OperationType.query: - default: - suffix = 'Query'; - break; - } + final operations = + document.definitions.whereType().toList(); - final rootTypeName = (schemaVisitor.schemaDefinitionNode?.operationTypes ?? - []) - .firstWhere((e) => e.operation == operation.type, orElse: () => null) - ?.type - ?.name - ?.value ?? - suffix; + return operations.map((operation) { + if (fragmentsCommon.isEmpty) { + fragments.addAll(documentFragments); + } else { + final fragmentsOperation = + _extractFragments(operation.selectionSet, fragmentsCommon); + document.definitions.addAll(fragmentsOperation); + fragments.addAll(fragmentsOperation); + } - if (rootTypeName == null) { - throw Exception( - '''No root type was found for ${operation.type} $operationName.'''); - } + final basename = p.basenameWithoutExtension(path).split('.').first; + final operationName = operation.name?.value ?? basename; + + final schemaVisitor = SchemaDefinitionVisitor(); + final objectVisitor = ObjectTypeDefinitionVisitor(); + + schema.accept(schemaVisitor); + schema.accept(objectVisitor); + + String suffix; + switch (operation.type) { + case OperationType.subscription: + suffix = 'Subscription'; + break; + case OperationType.mutation: + suffix = 'Mutation'; + break; + case OperationType.query: + default: + suffix = 'Query'; + break; + } - final TypeDefinitionNode parentType = objectVisitor.getByName(rootTypeName); - - final name = QueryName.fromPath( - path: createPathName([ - ClassName(name: operationName), - ClassName(name: parentType.name.value) - ], schemaMap.namingScheme)); - - final context = Context( - schema: schema, - options: options, - schemaMap: schemaMap, - path: [ - TypeName(name: operationName), - TypeName(name: parentType.name.value) - ], - currentType: parentType, - currentFieldName: null, - currentClassName: null, - generatedClasses: [], - inputsClasses: [], - fragments: fragments, - usedEnums: {}, - usedInputObjects: {}, - ); + final rootTypeName = + (schemaVisitor.schemaDefinitionNode?.operationTypes ?? []) + .firstWhere((e) => e.operation == operation.type, + orElse: () => null) + ?.type + ?.name + ?.value ?? + suffix; + + if (rootTypeName == null) { + throw Exception( + '''No root type was found for ${operation.type} $operationName.'''); + } - final visitor = _GeneratorVisitor( - context: context, - ); + final TypeDefinitionNode parentType = objectVisitor.getByName(rootTypeName); + + final name = QueryName.fromPath( + path: createPathName([ + ClassName(name: operationName), + ClassName(name: parentType.name.value) + ], schemaMap.namingScheme)); + + final context = Context( + schema: schema, + options: options, + schemaMap: schemaMap, + path: [ + TypeName(name: operationName), + TypeName(name: parentType.name.value) + ], + currentType: parentType, + currentFieldName: null, + currentClassName: null, + generatedClasses: [], + inputsClasses: [], + fragments: fragments, + usedEnums: {}, + usedInputObjects: {}, + ); - // scans schema for canonical types only once - if (_canonicalVisitor == null) { - _canonicalVisitor = _CanonicalVisitor( - context: context.sameTypeWithNoPath(), + final visitor = _GeneratorVisitor( + context: context, ); + // scans schema for canonical types only once + if (_canonicalVisitor == null) { + _canonicalVisitor = _CanonicalVisitor( + context: context.sameTypeWithNoPath(), + ); - schema.accept(_canonicalVisitor); - } + schema.accept(_canonicalVisitor); + } - document.accept(visitor); + DocumentNode( + definitions: document.definitions + // filtering unused operations + .where((e) => e is! OperationDefinitionNode || e == operation) + .toList(), + ).accept(visitor); - return QueryDefinition( - name: name, - operationName: operationName, - document: document, - classes: [ - ..._canonicalVisitor.enums - .where((e) => context.usedEnums.contains(e.name)), - ...visitor.context.generatedClasses, - ..._canonicalVisitor.inputObjects - .where((i) => context.usedInputObjects.contains(i.name)), - ], - inputs: visitor.context.inputsClasses, - generateHelpers: options.generateHelpers, - suffix: suffix, - ); + return QueryDefinition( + name: name, + operationName: operationName, + document: document, + classes: [ + ..._canonicalVisitor.enums + .where((e) => context.usedEnums.contains(e.name)), + ...visitor.context.generatedClasses, + ..._canonicalVisitor.inputObjects + .where((i) => context.usedInputObjects.contains(i.name)), + ], + inputs: visitor.context.inputsClasses, + generateHelpers: options.generateHelpers, + suffix: suffix, + ); + }); } List _extractCustomImports( diff --git a/pubspec.yaml b/pubspec.yaml index 55e39407..80559048 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: artemis -version: 6.5.2-beta.1 +version: 6.6.1-beta.1 authors: - Igor Borges diff --git a/test/query_generator/multiple_operations_per_file_test.dart b/test/query_generator/multiple_operations_per_file_test.dart new file mode 100644 index 00000000..eefae925 --- /dev/null +++ b/test/query_generator/multiple_operations_per_file_test.dart @@ -0,0 +1,505 @@ +import 'package:artemis/generator/data/data.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() { + group('On generation', () { + test( + 'Allows multiple mutations per file', + () async => testGenerator( + query: query, + schema: r''' + schema { + mutation: Mutation + query: Query + } + + type Mutation { + mut(input: Input!): MutationResponse + } + + type Query { + que(intsNonNullable: [Int]!, stringNullable: String): QueryResponse + } + + type QueryResponse { + s: String + i: Int + list(intsNonNullable: [Int]!): [Int]! + } + + type MutationResponse { + s: String + } + + input Input { + s: String! + } + ''', + libraryDefinition: libraryDefinition, + generatedFile: generatedFile, + generateHelpers: true, + ), + ); + }); +} + +const query = r''' +mutation MutData($input: Input!) { + mut(input: $input) { + s + } +} + +query QueData($intsNonNullable: [Int]!, $stringNullable: String) { + que(intsNonNullable: $intsNonNullable, stringNullable: $stringNullable) { + s + i + list(intsNonNullable: $intsNonNullable) + } +} +'''; + +final LibraryDefinition libraryDefinition = + LibraryDefinition(basename: r'query.graphql', queries: [ + QueryDefinition( + name: QueryName(name: r'MutData$_Mutation'), + operationName: r'MutData', + classes: [ + ClassDefinition( + name: ClassName(name: r'MutData$_Mutation$_MutationResponse'), + properties: [ + ClassProperty( + type: TypeName(name: r'String'), + name: ClassPropertyName(name: r's'), + isNonNull: false, + isResolveType: false) + ], + factoryPossibilities: {}, + typeNameField: TypeName(name: r'__typename'), + isInput: false), + ClassDefinition( + name: ClassName(name: r'MutData$_Mutation'), + properties: [ + ClassProperty( + type: TypeName(name: r'MutData$_Mutation$_MutationResponse'), + name: ClassPropertyName(name: r'mut'), + isNonNull: false, + isResolveType: false) + ], + factoryPossibilities: {}, + typeNameField: TypeName(name: r'__typename'), + isInput: false), + ClassDefinition( + name: ClassName(name: r'Input'), + properties: [ + ClassProperty( + type: TypeName(name: r'String'), + name: ClassPropertyName(name: r's'), + isNonNull: true, + isResolveType: false) + ], + factoryPossibilities: {}, + typeNameField: TypeName(name: r'__typename'), + isInput: true) + ], + inputs: [ + QueryInput( + type: TypeName(name: r'Input'), + name: QueryInputName(name: r'input'), + isNonNull: true) + ], + generateHelpers: true, + suffix: r'Mutation'), + QueryDefinition( + name: QueryName(name: r'QueData$_Query'), + operationName: r'QueData', + classes: [ + ClassDefinition( + name: ClassName(name: r'QueData$_Query$_QueryResponse'), + properties: [ + ClassProperty( + type: TypeName(name: r'String'), + name: ClassPropertyName(name: r's'), + isNonNull: false, + isResolveType: false), + ClassProperty( + type: TypeName(name: r'int'), + name: ClassPropertyName(name: r'i'), + isNonNull: false, + isResolveType: false), + ClassProperty( + type: TypeName(name: r'List'), + name: ClassPropertyName(name: r'list'), + isNonNull: true, + isResolveType: false) + ], + factoryPossibilities: {}, + typeNameField: TypeName(name: r'__typename'), + isInput: false), + ClassDefinition( + name: ClassName(name: r'QueData$_Query'), + properties: [ + ClassProperty( + type: TypeName(name: r'QueData$_Query$_QueryResponse'), + name: ClassPropertyName(name: r'que'), + isNonNull: false, + isResolveType: false) + ], + factoryPossibilities: {}, + typeNameField: TypeName(name: r'__typename'), + isInput: false) + ], + inputs: [ + QueryInput( + type: TypeName(name: r'List'), + name: QueryInputName(name: r'intsNonNullable'), + isNonNull: true), + QueryInput( + type: TypeName(name: r'String'), + name: QueryInputName(name: r'stringNullable'), + isNonNull: false) + ], + generateHelpers: true, + suffix: r'Query') +]); + +const generatedFile = r'''// GENERATED CODE - DO NOT MODIFY BY HAND + +import 'package:meta/meta.dart'; +import 'package:artemis/artemis.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:equatable/equatable.dart'; +import 'package:gql/ast.dart'; +part 'query.graphql.g.dart'; + +@JsonSerializable(explicitToJson: true) +class MutData$Mutation$MutationResponse with EquatableMixin { + MutData$Mutation$MutationResponse(); + + factory MutData$Mutation$MutationResponse.fromJson( + Map json) => + _$MutData$Mutation$MutationResponseFromJson(json); + + String s; + + @override + List get props => [s]; + Map toJson() => + _$MutData$Mutation$MutationResponseToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class MutData$Mutation with EquatableMixin { + MutData$Mutation(); + + factory MutData$Mutation.fromJson(Map json) => + _$MutData$MutationFromJson(json); + + MutData$Mutation$MutationResponse mut; + + @override + List get props => [mut]; + Map toJson() => _$MutData$MutationToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class Input with EquatableMixin { + Input({@required this.s}); + + factory Input.fromJson(Map json) => _$InputFromJson(json); + + String s; + + @override + List get props => [s]; + Map toJson() => _$InputToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class QueData$Query$QueryResponse with EquatableMixin { + QueData$Query$QueryResponse(); + + factory QueData$Query$QueryResponse.fromJson(Map json) => + _$QueData$Query$QueryResponseFromJson(json); + + String s; + + int i; + + List list; + + @override + List get props => [s, i, list]; + Map toJson() => _$QueData$Query$QueryResponseToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class QueData$Query with EquatableMixin { + QueData$Query(); + + factory QueData$Query.fromJson(Map json) => + _$QueData$QueryFromJson(json); + + QueData$Query$QueryResponse que; + + @override + List get props => [que]; + Map toJson() => _$QueData$QueryToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class MutDataArguments extends JsonSerializable with EquatableMixin { + MutDataArguments({@required this.input}); + + factory MutDataArguments.fromJson(Map json) => + _$MutDataArgumentsFromJson(json); + + final Input input; + + @override + List get props => [input]; + Map toJson() => _$MutDataArgumentsToJson(this); +} + +class MutDataMutation extends GraphQLQuery { + MutDataMutation({this.variables}); + + @override + final DocumentNode document = DocumentNode(definitions: [ + OperationDefinitionNode( + type: OperationType.mutation, + name: NameNode(value: 'MutData'), + variableDefinitions: [ + VariableDefinitionNode( + variable: VariableNode(name: NameNode(value: 'input')), + type: NamedTypeNode( + name: NameNode(value: 'Input'), isNonNull: true), + defaultValue: DefaultValueNode(value: null), + directives: []) + ], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FieldNode( + name: NameNode(value: 'mut'), + alias: null, + arguments: [ + ArgumentNode( + name: NameNode(value: 'input'), + value: VariableNode(name: NameNode(value: 'input'))) + ], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FieldNode( + name: NameNode(value: 's'), + alias: null, + arguments: [], + directives: [], + selectionSet: null) + ])) + ])), + OperationDefinitionNode( + type: OperationType.query, + name: NameNode(value: 'QueData'), + variableDefinitions: [ + VariableDefinitionNode( + variable: VariableNode(name: NameNode(value: 'intsNonNullable')), + type: ListTypeNode( + type: NamedTypeNode( + name: NameNode(value: 'Int'), isNonNull: false), + isNonNull: true), + defaultValue: DefaultValueNode(value: null), + directives: []), + VariableDefinitionNode( + variable: VariableNode(name: NameNode(value: 'stringNullable')), + type: NamedTypeNode( + name: NameNode(value: 'String'), isNonNull: false), + defaultValue: DefaultValueNode(value: null), + directives: []) + ], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FieldNode( + name: NameNode(value: 'que'), + alias: null, + arguments: [ + ArgumentNode( + name: NameNode(value: 'intsNonNullable'), + value: + VariableNode(name: NameNode(value: 'intsNonNullable'))), + ArgumentNode( + name: NameNode(value: 'stringNullable'), + value: + VariableNode(name: NameNode(value: 'stringNullable'))) + ], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FieldNode( + name: NameNode(value: 's'), + alias: null, + arguments: [], + directives: [], + selectionSet: null), + FieldNode( + name: NameNode(value: 'i'), + alias: null, + arguments: [], + directives: [], + selectionSet: null), + FieldNode( + name: NameNode(value: 'list'), + alias: null, + arguments: [ + ArgumentNode( + name: NameNode(value: 'intsNonNullable'), + value: VariableNode( + name: NameNode(value: 'intsNonNullable'))) + ], + directives: [], + selectionSet: null) + ])) + ])) + ]); + + @override + final String operationName = 'MutData'; + + @override + final MutDataArguments variables; + + @override + List get props => [document, operationName, variables]; + @override + MutData$Mutation parse(Map json) => + MutData$Mutation.fromJson(json); +} + +@JsonSerializable(explicitToJson: true) +class QueDataArguments extends JsonSerializable with EquatableMixin { + QueDataArguments({@required this.intsNonNullable, this.stringNullable}); + + factory QueDataArguments.fromJson(Map json) => + _$QueDataArgumentsFromJson(json); + + final List intsNonNullable; + + final String stringNullable; + + @override + List get props => [intsNonNullable, stringNullable]; + Map toJson() => _$QueDataArgumentsToJson(this); +} + +class QueDataQuery extends GraphQLQuery { + QueDataQuery({this.variables}); + + @override + final DocumentNode document = DocumentNode(definitions: [ + OperationDefinitionNode( + type: OperationType.mutation, + name: NameNode(value: 'MutData'), + variableDefinitions: [ + VariableDefinitionNode( + variable: VariableNode(name: NameNode(value: 'input')), + type: NamedTypeNode( + name: NameNode(value: 'Input'), isNonNull: true), + defaultValue: DefaultValueNode(value: null), + directives: []) + ], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FieldNode( + name: NameNode(value: 'mut'), + alias: null, + arguments: [ + ArgumentNode( + name: NameNode(value: 'input'), + value: VariableNode(name: NameNode(value: 'input'))) + ], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FieldNode( + name: NameNode(value: 's'), + alias: null, + arguments: [], + directives: [], + selectionSet: null) + ])) + ])), + OperationDefinitionNode( + type: OperationType.query, + name: NameNode(value: 'QueData'), + variableDefinitions: [ + VariableDefinitionNode( + variable: VariableNode(name: NameNode(value: 'intsNonNullable')), + type: ListTypeNode( + type: NamedTypeNode( + name: NameNode(value: 'Int'), isNonNull: false), + isNonNull: true), + defaultValue: DefaultValueNode(value: null), + directives: []), + VariableDefinitionNode( + variable: VariableNode(name: NameNode(value: 'stringNullable')), + type: NamedTypeNode( + name: NameNode(value: 'String'), isNonNull: false), + defaultValue: DefaultValueNode(value: null), + directives: []) + ], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FieldNode( + name: NameNode(value: 'que'), + alias: null, + arguments: [ + ArgumentNode( + name: NameNode(value: 'intsNonNullable'), + value: + VariableNode(name: NameNode(value: 'intsNonNullable'))), + ArgumentNode( + name: NameNode(value: 'stringNullable'), + value: + VariableNode(name: NameNode(value: 'stringNullable'))) + ], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FieldNode( + name: NameNode(value: 's'), + alias: null, + arguments: [], + directives: [], + selectionSet: null), + FieldNode( + name: NameNode(value: 'i'), + alias: null, + arguments: [], + directives: [], + selectionSet: null), + FieldNode( + name: NameNode(value: 'list'), + alias: null, + arguments: [ + ArgumentNode( + name: NameNode(value: 'intsNonNullable'), + value: VariableNode( + name: NameNode(value: 'intsNonNullable'))) + ], + directives: [], + selectionSet: null) + ])) + ])) + ]); + + @override + final String operationName = 'QueData'; + + @override + final QueDataArguments variables; + + @override + List get props => [document, operationName, variables]; + @override + QueData$Query parse(Map json) => + QueData$Query.fromJson(json); +} +''';