diff --git a/protographic/SDL_PROTO_RULES.md b/protographic/SDL_PROTO_RULES.md index b7c1da192c..814552f057 100644 --- a/protographic/SDL_PROTO_RULES.md +++ b/protographic/SDL_PROTO_RULES.md @@ -230,29 +230,116 @@ enum UserRole { } ``` -## Nested List Types +## List Types -For nested lists in GraphQL (e.g., `[[Type]]`), Protographic creates a wrapper message: +Protographic handles GraphQL list nullability by creating wrapper messages when needed, since Protocol Buffers doesn't natively support nullable lists or nested list structures. + +### Core Concepts + +- **Non-nullable single-level lists**: Use the `repeated` keyword directly +- **Nullable lists**: Wrapped in `ListOf{Type}` messages +- **Nested lists**: Always use wrapper messages with multiple `ListOf` prefixes based on nesting level (e.g., `ListOfListOfString`) +- **Nullable list items**: Currently ignored (no wrapper generated for item nullability) + +### Non-Nullable Single Lists +Non-nullable lists use `repeated` fields directly: + +```graphql +type User { + tags: [String!]! +} +``` + +Maps to: + +```protobuf +message User { + repeated string tags = 1; +} +``` + +### Nullable Single Lists + +Nullable lists require wrapper messages: ```graphql -type Matrix { - values: [[Int!]!]! +type User { + optionalTags: [String] } ``` Maps to: ```protobuf -message IntList { - repeated int32 result = 1; +message ListOfString { + repeated string items = 1; } -message Matrix { - repeated IntList values = 1; +message User { + ListOfString optional_tags = 1; } ``` -This approach is used for any nested list, regardless of the depth of nesting. For complex nested types, wrapper messages are created automatically with the naming convention of `{BaseType}List`. +### Non-Nullable Nested Lists + +Non-nullable nested lists always use wrapper messages to preserve inner list nullability: + +```graphql +type User { + categories: [[String!]!]! +} +``` + +Maps to: + +```protobuf +message ListOfString { + repeated string items = 1; +} + +message ListOfListOfString { + message List { + repeated ListOfString items = 1; + } + List list = 1; +} + +message User { + ListOfListOfString categories = 1; +} +``` + +### Nullable Nested Lists + +Nullable nested lists use nested wrapper messages: + +```graphql +type User { + posts: [[String]] +} +``` + +Maps to: + +```protobuf +message ListOfString { + repeated string items = 1; +} + +message ListOfListOfString { + message List { + repeated ListOfString items = 1; + } + List list = 1; +} + +message User { + ListOfListOfString posts = 1; +} +``` + + + ## Field Numbering and Stability diff --git a/protographic/src/sdl-to-proto-visitor.ts b/protographic/src/sdl-to-proto-visitor.ts index 91a4507f58..eea6c39e8b 100644 --- a/protographic/src/sdl-to-proto-visitor.ts +++ b/protographic/src/sdl-to-proto-visitor.ts @@ -6,7 +6,10 @@ import { GraphQLField, GraphQLInputObjectType, GraphQLInterfaceType, + GraphQLList, GraphQLNamedType, + GraphQLNonNull, + GraphQLNullableType, GraphQLObjectType, GraphQLSchema, GraphQLType, @@ -84,6 +87,14 @@ export interface GraphQLToProtoTextVisitorOptions { includeComments?: boolean; } +/** + * Data structure for formatting message fields + */ +interface ProtoType { + typeName: string; + isRepeated: boolean; +} + /** * Visitor that converts GraphQL SDL to Protocol Buffer text definition * @@ -882,11 +893,10 @@ Example: } // Check if the argument is a list type and add the repeated keyword if needed - const isRepeated = isListType(arg.type) || (isNonNullType(arg.type) && isListType(arg.type.ofType)); - if (isRepeated) { - messageLines.push(` repeated ${argType} ${argProtoName} = ${fieldNumber};`); + if (argType.isRepeated) { + messageLines.push(` repeated ${argType.typeName} ${argProtoName} = ${fieldNumber};`); } else { - messageLines.push(` ${argType} ${argProtoName} = ${fieldNumber};`); + messageLines.push(` ${argType.typeName} ${argProtoName} = ${fieldNumber};`); } // Add complex input types to the queue for processing @@ -940,8 +950,6 @@ Example: } const returnType = this.getProtoTypeFromGraphQL(field.type); - const isRepeated = isListType(field.type) || (isNonNullType(field.type) && isListType(field.type.ofType)); - // Get the appropriate field number, respecting the lock const fieldNumber = this.getFieldNumber(responseName, protoFieldName, 1); @@ -951,10 +959,10 @@ Example: messageLines.push(...this.formatComment(field.description, 1)); } - if (isRepeated) { - messageLines.push(` repeated ${returnType} ${protoFieldName} = ${fieldNumber};`); + if (returnType.isRepeated) { + messageLines.push(` repeated ${returnType.typeName} ${protoFieldName} = ${fieldNumber};`); } else { - messageLines.push(` ${returnType} ${protoFieldName} = ${fieldNumber};`); + messageLines.push(` ${returnType.typeName} ${protoFieldName} = ${fieldNumber};`); } messageLines.push('}'); @@ -1105,7 +1113,6 @@ Example: const field = fields[fieldName]; const fieldType = this.getProtoTypeFromGraphQL(field.type); - const isRepeated = isListType(field.type) || (isNonNullType(field.type) && isListType(field.type.ofType)); const protoFieldName = graphqlFieldToProtoField(fieldName); // Get the appropriate field number, respecting the lock @@ -1116,10 +1123,10 @@ Example: this.protoText.push(...this.formatComment(field.description, 1)); // Field comment, indent 1 level } - if (isRepeated) { - this.protoText.push(` repeated ${fieldType} ${protoFieldName} = ${fieldNumber};`); + if (fieldType.isRepeated) { + this.protoText.push(` repeated ${fieldType.typeName} ${protoFieldName} = ${fieldNumber};`); } else { - this.protoText.push(` ${fieldType} ${protoFieldName} = ${fieldNumber};`); + this.protoText.push(` ${fieldType.typeName} ${protoFieldName} = ${fieldNumber};`); } // Queue complex field types for processing @@ -1177,7 +1184,6 @@ Example: const field = fields[fieldName]; const fieldType = this.getProtoTypeFromGraphQL(field.type); - const isRepeated = isListType(field.type) || (isNonNullType(field.type) && isListType(field.type.ofType)); const protoFieldName = graphqlFieldToProtoField(fieldName); // Get the appropriate field number, respecting the lock @@ -1188,10 +1194,10 @@ Example: this.protoText.push(...this.formatComment(field.description, 1)); // Field comment, indent 1 level } - if (isRepeated) { - this.protoText.push(` repeated ${fieldType} ${protoFieldName} = ${fieldNumber};`); + if (fieldType.isRepeated) { + this.protoText.push(` repeated ${fieldType.typeName} ${protoFieldName} = ${fieldNumber};`); } else { - this.protoText.push(` ${fieldType} ${protoFieldName} = ${fieldNumber};`); + this.protoText.push(` ${fieldType.typeName} ${protoFieldName} = ${fieldNumber};`); } // Queue complex field types for processing @@ -1413,117 +1419,201 @@ Example: * @param ignoreWrapperTypes - If true, do not use wrapper types for nullable scalar fields * @returns The corresponding Protocol Buffer type name */ - private getProtoTypeFromGraphQL(graphqlType: GraphQLType, ignoreWrapperTypes: boolean = false): string { + private getProtoTypeFromGraphQL(graphqlType: GraphQLType, ignoreWrapperTypes: boolean = false): ProtoType { + // Nullable lists need to be handled first, otherwise they will be treated as scalar types + if (isListType(graphqlType) || (isNonNullType(graphqlType) && isListType(graphqlType.ofType))) { + return this.handleListType(graphqlType); + } // For nullable scalar types, use wrapper types if (isScalarType(graphqlType)) { if (ignoreWrapperTypes) { - return SCALAR_TYPE_MAP[graphqlType.name] || 'string'; + return { typeName: SCALAR_TYPE_MAP[graphqlType.name] || 'string', isRepeated: false }; } this.usesWrapperTypes = true; // Track that we're using wrapper types - return SCALAR_WRAPPER_TYPE_MAP[graphqlType.name] || 'google.protobuf.StringValue'; + return { + typeName: SCALAR_WRAPPER_TYPE_MAP[graphqlType.name] || 'google.protobuf.StringValue', + isRepeated: false, + }; } if (isEnumType(graphqlType)) { - return graphqlType.name; + return { typeName: graphqlType.name, isRepeated: false }; } if (isNonNullType(graphqlType)) { // For non-null scalar types, use the base type if (isScalarType(graphqlType.ofType)) { - return SCALAR_TYPE_MAP[graphqlType.ofType.name] || 'string'; + return { typeName: SCALAR_TYPE_MAP[graphqlType.ofType.name] || 'string', isRepeated: false }; } return this.getProtoTypeFromGraphQL(graphqlType.ofType); } + // Named types (object, interface, union, input) + const namedType = graphqlType as GraphQLNamedType; + if (namedType && typeof namedType.name === 'string') { + return { typeName: namedType.name, isRepeated: false }; + } - if (isListType(graphqlType)) { - // Handle nested list types (e.g., [[Type]]) - const innerType = graphqlType.ofType; + return { typeName: 'string', isRepeated: false }; // Default fallback + } - // If the inner type is also a list, we need to use a wrapper message - if (isListType(innerType) || (isNonNullType(innerType) && isListType(innerType.ofType))) { - // Find the most inner type by unwrapping all lists and non-nulls - let currentType: GraphQLType = innerType; - while (isListType(currentType) || isNonNullType(currentType)) { - currentType = isListType(currentType) ? currentType.ofType : (currentType as any).ofType; - } + /** + * Converts GraphQL list types to appropriate Protocol Buffer representations. + * + * For non-nullable, single-level lists (e.g., [String!]!), generates simple repeated fields. + * For nullable lists (e.g., [String]) or nested lists (e.g., [[String]]), creates wrapper + * messages to properly handle nullability in proto3. + * + * Examples: + * - [String!]! → repeated string field_name = 1; + * - [String] → ListOfString field_name = 1; (with wrapper message) + * - [[String!]!]! → ListOfListOfString field_name = 1; (with nested wrapper messages) + * - [[String]] → ListOfListOfString field_name = 1; (with nested wrapper messages) + * + * @param graphqlType - The GraphQL list type to convert + * @returns ProtoType object containing the type name and whether it should be repeated + */ + private handleListType(graphqlType: GraphQLList | GraphQLNonNull>): ProtoType { + const listType = this.unwrapNonNullType(graphqlType); + const isNullableList = !isNonNullType(graphqlType); + const isNestedList = this.isNestedListType(listType); - // Get the name of the inner type and create wrapper name - const namedInnerType = currentType as GraphQLNamedType; - const wrapperName = `${namedInnerType.name}List`; + // Simple non-nullable lists can use repeated fields directly + if (!isNullableList && !isNestedList) { + return { ...this.getProtoTypeFromGraphQL(getNamedType(listType), true), isRepeated: true }; + } - // Generate the wrapper message if not already created - if (!this.processedTypes.has(wrapperName) && !this.nestedListWrappers.has(wrapperName)) { - this.createNestedListWrapper(wrapperName, namedInnerType); - } + // Nullable or nested lists need wrapper messages + const baseType = getNamedType(listType); + const nestingLevel = this.calculateNestingLevel(listType); - return wrapperName; - } + // For nested lists, always use full nesting level to preserve inner list nullability + // For single-level nullable lists, use nesting level 1 + const wrapperNestingLevel = isNestedList ? nestingLevel : 1; - return this.getProtoTypeFromGraphQL(innerType, true); + // Generate all required wrapper messages + let wrapperName = ''; + for (let i = 1; i <= wrapperNestingLevel; i++) { + wrapperName = this.createNestedListWrapper(i, baseType); } - // Named types (object, interface, union, input) - const namedType = graphqlType as GraphQLNamedType; - if (namedType && typeof namedType.name === 'string') { - return namedType.name; + // For nested lists, never use repeated at field level to preserve nullability + return { typeName: wrapperName, isRepeated: false }; + } + + /** + * Unwraps a GraphQL type from a GraphQLNonNull type + */ + private unwrapNonNullType(graphqlType: T | GraphQLNonNull): T { + return isNonNullType(graphqlType) ? (graphqlType.ofType as T) : graphqlType; + } + + /** + * Checks if a GraphQL list type contains nested lists + * Type guard that narrows the input type when nested lists are detected + */ + private isNestedListType( + listType: GraphQLList, + ): listType is GraphQLList | GraphQLNonNull>> { + return isListType(listType.ofType) || (isNonNullType(listType.ofType) && isListType(listType.ofType.ofType)); + } + + /** + * Calculates the nesting level of a GraphQL list type + */ + private calculateNestingLevel(listType: GraphQLList): number { + let level = 1; + let currentType: GraphQLType = listType.ofType; + + while (true) { + if (isNonNullType(currentType)) { + currentType = currentType.ofType; + } else if (isListType(currentType)) { + currentType = currentType.ofType; + level++; + } else { + break; + } } - return 'string'; // Default fallback + return level; } /** - * Create a nested list wrapper message for the given base type + * Creates wrapper messages for nullable or nested GraphQL lists. + * + * Generates Protocol Buffer message definitions to handle list nullability and nesting. + * The wrapper messages are stored and later included in the final proto output. + * + * For level 1: Creates simple wrapper like: + * message ListOfString { + * repeated string items = 1; + * } + * + * For level > 1: Creates nested wrapper structures like: + * message ListOfListOfString { + * message List { + * repeated ListOfString items = 1; + * } + * List list = 1; + * } + * + * @param level - The nesting level (1 for simple wrapper, >1 for nested structures) + * @param baseType - The GraphQL base type being wrapped (e.g., String, User, etc.) + * @returns The generated wrapper message name (e.g., "ListOfString", "ListOfListOfUser") */ - private createNestedListWrapper(wrapperName: string, baseType: GraphQLNamedType): void { - // Skip if already processed + private createNestedListWrapper(level: number, baseType: GraphQLNamedType): string { + const wrapperName = `${'ListOf'.repeat(level)}${baseType.name}`; + + // Return existing wrapper if already created if (this.processedTypes.has(wrapperName) || this.nestedListWrappers.has(wrapperName)) { - return; + return wrapperName; } - // Mark as processed to avoid recursion this.processedTypes.add(wrapperName); - // Check for field removals if lock data exists for this wrapper - const lockData = this.lockManager.getLockData(); - if (lockData.messages[wrapperName]) { - const originalFieldNames = Object.keys(lockData.messages[wrapperName].fields); - const currentFieldNames = ['result']; - this.trackRemovedFields(wrapperName, originalFieldNames, currentFieldNames); - } - - // Create a temporary array for the wrapper definition - const messageLines: string[] = []; + const messageLines = this.buildWrapperMessage(wrapperName, level, baseType); + this.nestedListWrappers.set(wrapperName, messageLines.join('\n')); - // Add a description comment for the wrapper message - if (this.includeComments) { - const wrapperComment = `Wrapper message for a list of ${baseType.name}.`; - messageLines.push(...this.formatComment(wrapperComment, 0)); // Top-level comment, no indent - } + return wrapperName; + } - messageLines.push(`message ${wrapperName} {`); + /** + * Builds the message lines for a wrapper message + */ + private buildWrapperMessage(wrapperName: string, level: number, baseType: GraphQLNamedType): string[] { + const lines: string[] = []; - // Add reserved field numbers if any exist - const messageLock = lockData.messages[wrapperName]; - if (messageLock?.reservedNumbers && messageLock.reservedNumbers.length > 0) { - messageLines.push(` reserved ${this.formatReservedNumbers(messageLock.reservedNumbers)};`); + // Add comment if enabled + if (this.includeComments) { + lines.push(...this.formatComment(`Wrapper message for a list of ${baseType.name}.`, 0)); } - // Get the appropriate field number from the lock - const fieldNumber = this.getFieldNumber(wrapperName, 'result', 1); + lines.push(`message ${wrapperName} {`); - // For the inner type, we need to get the proto type for the base type - const protoType = this.getProtoTypeFromGraphQL(baseType, true); - messageLines.push(` repeated ${protoType} result = ${fieldNumber};`); + const formatIndent = (indent: number, content: string) => { + return ' '.repeat(indent) + content; + }; - messageLines.push('}'); - messageLines.push(''); + if (level > 1) { + // Nested structure for deep lists + const innerWrapperName = `${'ListOf'.repeat(level - 1)}${baseType.name}`; + lines.push( + formatIndent(1, `message List {`), + formatIndent(2, `repeated ${innerWrapperName} items = 1;`), + formatIndent(1, `}`), + ); - // Ensure the wrapper message is registered in the lock manager data - this.lockManager.reconcileMessageFieldOrder(wrapperName, ['result']); + // Wrapper types always use deterministic field numbers - 'list' field is always 1 + lines.push(formatIndent(1, `List list = 1;`)); + } else { + // Simple repeated field for level 1 - 'items' field is always 1 + const protoType = this.getProtoTypeFromGraphQL(baseType, true); + lines.push(formatIndent(1, `repeated ${protoType.typeName} items = 1;`)); + } - // Store the wrapper message for later inclusion in the output - this.nestedListWrappers.set(wrapperName, messageLines.join('\n')); + lines.push('}', ''); + return lines; } /** diff --git a/protographic/tests/sdl-to-proto/01-basic-types.test.ts b/protographic/tests/sdl-to-proto/01-basic-types.test.ts index c77a85a517..539b9da252 100644 --- a/protographic/tests/sdl-to-proto/01-basic-types.test.ts +++ b/protographic/tests/sdl-to-proto/01-basic-types.test.ts @@ -140,12 +140,17 @@ describe('SDL to Proto - Basic Types', () => { rpc QueryStringList(QueryStringListRequest) returns (QueryStringListResponse) {} } + // Wrapper message for a list of String. + message ListOfString { + repeated string items = 1; + } + // Request message for stringList operation. message QueryStringListRequest { } // Response message for stringList operation. message QueryStringListResponse { - repeated string string_list = 1; + ListOfString string_list = 1; } // Request message for intList operation. message QueryIntListRequest { @@ -240,6 +245,11 @@ describe('SDL to Proto - Basic Types', () => { rpc QueryUser(QueryUserRequest) returns (QueryUserResponse) {} } + // Wrapper message for a list of User. + message ListOfUser { + repeated User items = 1; + } + // Request message for user operation. message QueryUserRequest { string id = 1; @@ -256,7 +266,7 @@ describe('SDL to Proto - Basic Types', () => { } // Response message for filteredUsers operation. message QueryFilteredUsersResponse { - repeated User filtered_users = 1; + ListOfUser filtered_users = 1; } message User { @@ -371,7 +381,7 @@ describe('SDL to Proto - Basic Types', () => { type Query { categoriesByKinds(kinds: [CategoryKind!]!): [Category!]! - filterItems(ids: [ID!], tags: [String]): [String] + filterItems(ids: [ID!]!, tags: [String]!): [String!]! } `; @@ -467,18 +477,47 @@ describe('SDL to Proto - Basic Types', () => { } // Wrapper message for a list of Float. - message FloatList { - repeated double result = 1; + message ListOfFloat { + repeated double items = 1; + } + + // Wrapper message for a list of Int. + message ListOfInt { + repeated int32 items = 1; + } + + // Wrapper message for a list of Float. + message ListOfListOfFloat { + message List { + repeated ListOfFloat items = 1; + } + List list = 1; } // Wrapper message for a list of Int. - message IntList { - repeated int32 result = 1; + message ListOfListOfInt { + message List { + repeated ListOfInt items = 1; + } + List list = 1; } // Wrapper message for a list of Point. - message PointList { - repeated Point result = 1; + message ListOfListOfPoint { + message List { + repeated ListOfPoint items = 1; + } + List list = 1; + } + + // Wrapper message for a list of Point. + message ListOfPoint { + repeated Point items = 1; + } + + // Wrapper message for a list of String. + message ListOfString { + repeated string items = 1; } // Request message for getMatrix operation. @@ -490,23 +529,23 @@ describe('SDL to Proto - Basic Types', () => { } // Request message for processMatrix operation. message QueryProcessMatrixRequest { - repeated FloatList matrix = 1; + ListOfListOfFloat matrix = 1; } // Response message for processMatrix operation. message QueryProcessMatrixResponse { - repeated IntList process_matrix = 1; + ListOfListOfInt process_matrix = 1; } // Request message for transformData operation. message QueryTransformDataRequest { - repeated PointList points = 1; + ListOfListOfPoint points = 1; } // Response message for transformData operation. message QueryTransformDataResponse { - repeated string transform_data = 1; + ListOfString transform_data = 1; } message Matrix { - repeated IntList values = 1; + ListOfListOfInt values = 1; repeated string labels = 2; } @@ -662,6 +701,16 @@ describe('SDL to Proto - Basic Types', () => { rpc QueryUser(QueryUserRequest) returns (QueryUserResponse) {} } + // Wrapper message for a list of TreeNode. + message ListOfTreeNode { + repeated TreeNode items = 1; + } + + // Wrapper message for a list of User. + message ListOfUser { + repeated User items = 1; + } + // Request message for user operation. message QueryUserRequest { string id = 1; @@ -702,7 +751,7 @@ describe('SDL to Proto - Basic Types', () => { string name = 2; google.protobuf.StringValue email = 3; UserProfile profile = 4; - repeated User friends = 5; + ListOfUser friends = 5; } message TreeNode { @@ -710,7 +759,7 @@ describe('SDL to Proto - Basic Types', () => { google.protobuf.StringValue value = 2; google.protobuf.DoubleValue weight = 3; bool is_leaf = 4; - repeated TreeNode children = 5; + ListOfTreeNode children = 5; TreeNode parent = 6; } diff --git a/protographic/tests/sdl-to-proto/02-complex-types.test.ts b/protographic/tests/sdl-to-proto/02-complex-types.test.ts index 58a80ccaa0..d95bd26fa6 100644 --- a/protographic/tests/sdl-to-proto/02-complex-types.test.ts +++ b/protographic/tests/sdl-to-proto/02-complex-types.test.ts @@ -36,13 +36,18 @@ describe('SDL to Proto - Complex Types', () => { rpc QueryUsersByRole(QueryUsersByRoleRequest) returns (QueryUsersByRoleResponse) {} } + // Wrapper message for a list of User. + message ListOfUser { + repeated User items = 1; + } + // Request message for usersByRole operation. message QueryUsersByRoleRequest { UserRole role = 1; } // Response message for usersByRole operation. message QueryUsersByRoleResponse { - repeated User users_by_role = 1; + ListOfUser users_by_role = 1; } message User { @@ -224,6 +229,11 @@ describe('SDL to Proto - Complex Types', () => { rpc QueryRootNode(QueryRootNodeRequest) returns (QueryRootNodeResponse) {} } + // Wrapper message for a list of TreeNode. + message ListOfTreeNode { + repeated TreeNode items = 1; + } + // Request message for rootNode operation. message QueryRootNodeRequest { } @@ -244,7 +254,7 @@ describe('SDL to Proto - Complex Types', () => { string id = 1; string value = 2; TreeNode parent = 3; - repeated TreeNode children = 4; + ListOfTreeNode children = 4; }" `); }); @@ -293,20 +303,30 @@ describe('SDL to Proto - Complex Types', () => { rpc QueryUsers(QueryUsersRequest) returns (QueryUsersResponse) {} } + // Wrapper message for a list of AddressInput. + message ListOfAddressInput { + repeated AddressInput items = 1; + } + + // Wrapper message for a list of User. + message ListOfUser { + repeated User items = 1; + } + // Request message for users operation. message QueryUsersRequest { UserFilterInput filter = 1; } // Response message for users operation. message QueryUsersResponse { - repeated User users = 1; + ListOfUser users = 1; } message UserFilterInput { google.protobuf.StringValue name_contains = 1; google.protobuf.Int32Value min_age = 2; google.protobuf.Int32Value max_age = 3; - repeated AddressInput addresses = 4; + ListOfAddressInput addresses = 4; } message User { diff --git a/protographic/tests/sdl-to-proto/05-edge-cases.test.ts b/protographic/tests/sdl-to-proto/05-edge-cases.test.ts index de21cd0c2d..3b9b1a9b6c 100644 --- a/protographic/tests/sdl-to-proto/05-edge-cases.test.ts +++ b/protographic/tests/sdl-to-proto/05-edge-cases.test.ts @@ -364,6 +364,21 @@ describe('SDL to Proto - Edge Cases and Error Handling', () => { rpc QueryUsers(QueryUsersRequest) returns (QueryUsersResponse) {} } + // Wrapper message for a list of Comment. + message ListOfComment { + repeated Comment items = 1; + } + + // Wrapper message for a list of Post. + message ListOfPost { + repeated Post items = 1; + } + + // Wrapper message for a list of String. + message ListOfString { + repeated string items = 1; + } + // Key message for User entity lookup message LookupUserByIdRequestKey { // Key field for User entity lookup. @@ -552,7 +567,7 @@ describe('SDL to Proto - Edge Cases and Error Handling', () => { string created_at = 4; google.protobuf.StringValue metadata = 5; UserStatus status = 6; - repeated Post posts = 7; + ListOfPost posts = 7; UserProfile profile = 8; } @@ -561,11 +576,11 @@ describe('SDL to Proto - Edge Cases and Error Handling', () => { string title = 2; string content = 3; User author = 4; - repeated string tags = 5; + ListOfString tags = 5; string created_at = 6; google.protobuf.StringValue updated_at = 7; PostStatus status = 8; - repeated Comment comments = 9; + ListOfComment comments = 9; } message Comment { @@ -581,7 +596,7 @@ describe('SDL to Proto - Edge Cases and Error Handling', () => { string query = 1; google.protobuf.Int32Value limit = 2; google.protobuf.Int32Value offset = 3; - repeated string types = 4; + ListOfString types = 4; } message SearchResult { @@ -608,7 +623,7 @@ describe('SDL to Proto - Edge Cases and Error Handling', () => { message PostInput { string title = 1; string content = 2; - repeated string tags = 3; + ListOfString tags = 3; PostStatus status = 4; } diff --git a/protographic/tests/sdl-to-proto/06-field-ordering.test.ts b/protographic/tests/sdl-to-proto/06-field-ordering.test.ts index e70fea24f4..be876dc6f2 100644 --- a/protographic/tests/sdl-to-proto/06-field-ordering.test.ts +++ b/protographic/tests/sdl-to-proto/06-field-ordering.test.ts @@ -10,6 +10,7 @@ import { getServiceMethods, getReservedNumbers, } from '../util'; +import { isNull } from 'lodash-es'; describe('Field Ordering and Preservation', () => { describe('Basic Message Field Ordering', () => { @@ -1123,4 +1124,320 @@ describe('Field Ordering and Preservation', () => { expect(priceRangeFields2['currency']).toBe(currencyNumber); }); }); + + describe('List Wrapper Types Field Ordering', () => { + test('should preserve field numbers for simple list wrapper types', () => { + // Initial schema with nullable lists that generate simple wrapper types + const initialSchema = buildSchema(` + type User { + id: ID! + name: String! + tags: [String] # nullable list -> generates ListOfString wrapper + scores: [Int] # nullable list -> generates ListOfInt wrapper + categories: [User] # nullable list -> generates ListOfUser wrapper + } + + type Query { + getUsers: [User] + } + `); + + // Create the visitor with no initial lock data + const visitor1 = new GraphQLToProtoTextVisitor(initialSchema, { + serviceName: 'UserService', + }); + + // Generate the first proto + const proto1 = visitor1.visit(); + + // Parse the proto with protobufjs + const root1 = loadProtoFromText(proto1); + + // Verify wrapper types exist and get their field numbers + const listOfStringFields = getFieldNumbersFromMessage(root1, 'ListOfString'); + const listOfIntFields = getFieldNumbersFromMessage(root1, 'ListOfInt'); + const listOfUserFields = getFieldNumbersFromMessage(root1, 'ListOfUser'); + + // Store original field numbers for wrapper types + const stringItemsNumber = listOfStringFields['items']; + const intItemsNumber = listOfIntFields['items']; + const userItemsNumber = listOfUserFields['items']; + + // Verify all wrapper types have the 'items' field with field number 1 + expect(stringItemsNumber).toBe(1); + expect(intItemsNumber).toBe(1); + expect(userItemsNumber).toBe(1); + + // Get the generated lock data + const lockData = visitor1.getGeneratedLockData(); + expect(lockData).not.toBeNull(); + + // Verify wrapper types are NOT in lock data (they're auto-generated with deterministic field numbers) + expect(lockData!.messages['ListOfString']).toBeUndefined(); + expect(lockData!.messages['ListOfInt']).toBeUndefined(); + expect(lockData!.messages['ListOfUser']).toBeUndefined(); + + // Modified schema with additional nullable lists (triggers regeneration) + const modifiedSchema = buildSchema(` + type User { + id: ID! + name: String! + tags: [String] # existing nullable list + scores: [Int] # existing nullable list + categories: [User] # existing nullable list + ratings: [Float] # new nullable list -> generates ListOfFloat wrapper + } + + type Query { + getUsers: [User] + } + `); + + // Create another visitor using the generated lock data + const visitor2 = new GraphQLToProtoTextVisitor(modifiedSchema, { + serviceName: 'UserService', + lockData: lockData || undefined, + }); + + // Generate the second proto + const proto2 = visitor2.visit(); + + // Parse the proto with protobufjs + const root2 = loadProtoFromText(proto2); + + // Verify existing wrapper types preserved their field numbers + const listOfStringFields2 = getFieldNumbersFromMessage(root2, 'ListOfString'); + const listOfIntFields2 = getFieldNumbersFromMessage(root2, 'ListOfInt'); + const listOfUserFields2 = getFieldNumbersFromMessage(root2, 'ListOfUser'); + const listOfFloatFields2 = getFieldNumbersFromMessage(root2, 'ListOfFloat'); + + // Verify field numbers are preserved + expect(listOfStringFields2['items']).toBe(stringItemsNumber); + expect(listOfIntFields2['items']).toBe(intItemsNumber); + expect(listOfUserFields2['items']).toBe(userItemsNumber); + + // Verify new wrapper type has field number 1 + expect(listOfFloatFields2['items']).toBe(1); + }); + + test('should preserve field numbers for nested list wrapper types', () => { + // Initial schema with nested lists that generate nested wrapper types + const initialSchema = buildSchema(` + type User { + id: ID! + name: String! + tagGroups: [[String]] # nested nullable list -> generates ListOfListOfString wrapper + scoreMatrix: [[Int]] # nested nullable list -> generates ListOfListOfInt wrapper + } + + type Query { + getUsers: [User] + } + `); + + // Create the visitor with no initial lock data + const visitor1 = new GraphQLToProtoTextVisitor(initialSchema, { + serviceName: 'UserService', + }); + + // Generate the first proto + const proto1 = visitor1.visit(); + + // Parse the proto with protobufjs + const root1 = loadProtoFromText(proto1); + + // Verify nested wrapper types exist and get their field numbers + const listOfListOfStringFields = getFieldNumbersFromMessage(root1, 'ListOfListOfString'); + const listOfListOfIntFields = getFieldNumbersFromMessage(root1, 'ListOfListOfInt'); + + // For nested wrappers, they should have a 'list' field (not 'items') + const stringListNumber = listOfListOfStringFields['list']; + const intListNumber = listOfListOfIntFields['list']; + + // Verify nested wrapper types have the 'list' field with field number 1 + expect(stringListNumber).toBe(1); + expect(intListNumber).toBe(1); + + // Also verify the inner simple wrapper types exist + const listOfStringFields = getFieldNumbersFromMessage(root1, 'ListOfString'); + const listOfIntFields = getFieldNumbersFromMessage(root1, 'ListOfInt'); + + const stringItemsNumber = listOfStringFields['items']; + const intItemsNumber = listOfIntFields['items']; + + expect(stringItemsNumber).toBe(1); + expect(intItemsNumber).toBe(1); + + // Get the generated lock data + const lockData = visitor1.getGeneratedLockData(); + expect(lockData).not.toBeNull(); + + // Verify wrapper types are NOT in lock data (they're auto-generated with deterministic field numbers) + expect(lockData!.messages['ListOfListOfString']).toBeUndefined(); + expect(lockData!.messages['ListOfListOfInt']).toBeUndefined(); + expect(lockData!.messages['ListOfString']).toBeUndefined(); + expect(lockData!.messages['ListOfInt']).toBeUndefined(); + + // Modified schema with additional nested lists + const modifiedSchema = buildSchema(` + type User { + id: ID! + name: String! + tagGroups: [[String]] # existing nested nullable list + scoreMatrix: [[Int]] # existing nested nullable list + userGroups: [[User]] # new nested nullable list -> generates ListOfListOfUser wrapper + } + + type Query { + getUsers: [User] + } + `); + + // Create another visitor using the generated lock data + const visitor2 = new GraphQLToProtoTextVisitor(modifiedSchema, { + serviceName: 'UserService', + lockData: lockData || undefined, + }); + + // Generate the second proto + const proto2 = visitor2.visit(); + + // Parse the proto with protobufjs + const root2 = loadProtoFromText(proto2); + + // Verify existing wrapper types preserved their field numbers + const listOfListOfStringFields2 = getFieldNumbersFromMessage(root2, 'ListOfListOfString'); + const listOfListOfIntFields2 = getFieldNumbersFromMessage(root2, 'ListOfListOfInt'); + const listOfListOfUserFields2 = getFieldNumbersFromMessage(root2, 'ListOfListOfUser'); + + // Verify existing field numbers are preserved + expect(listOfListOfStringFields2['list']).toBe(stringListNumber); + expect(listOfListOfIntFields2['list']).toBe(intListNumber); + + // Verify new nested wrapper type has field number 1 + expect(listOfListOfUserFields2['list']).toBe(1); + + // Verify simple wrapper types are still preserved + const listOfStringFields2 = getFieldNumbersFromMessage(root2, 'ListOfString'); + const listOfIntFields2 = getFieldNumbersFromMessage(root2, 'ListOfInt'); + const listOfUserFields2 = getFieldNumbersFromMessage(root2, 'ListOfUser'); + + expect(listOfStringFields2['items']).toBe(stringItemsNumber); + expect(listOfIntFields2['items']).toBe(intItemsNumber); + expect(listOfUserFields2['items']).toBe(1); // New simple wrapper for User + }); + + test('should handle mixed simple and nested wrapper types with field preservation', () => { + // Initial schema with both simple and nested nullable lists + const initialSchema = buildSchema(` + type User { + id: ID! + name: String! + tags: [String] # simple nullable list -> ListOfString + nestedTags: [[String]] # nested nullable list -> ListOfListOfString + friends: [User] # simple nullable list -> ListOfUser + friendGroups: [[User]] # nested nullable list -> ListOfListOfUser + } + + type Query { + getUsers: [User] + } + `); + + // Create the visitor with no initial lock data + const visitor1 = new GraphQLToProtoTextVisitor(initialSchema, { + serviceName: 'UserService', + }); + + // Generate the first proto + const proto1 = visitor1.visit(); + + // Parse the proto with protobufjs + const root1 = loadProtoFromText(proto1); + + // Get field numbers for all wrapper types + const listOfStringFields = getFieldNumbersFromMessage(root1, 'ListOfString'); + const listOfListOfStringFields = getFieldNumbersFromMessage(root1, 'ListOfListOfString'); + const listOfUserFields = getFieldNumbersFromMessage(root1, 'ListOfUser'); + const listOfListOfUserFields = getFieldNumbersFromMessage(root1, 'ListOfListOfUser'); + + // Store original field numbers + const stringItemsNumber = listOfStringFields['items']; + const stringListNumber = listOfListOfStringFields['list']; + const userItemsNumber = listOfUserFields['items']; + const userListNumber = listOfListOfUserFields['list']; + + // Verify correct field types for different wrapper levels + expect(stringItemsNumber).toBe(1); // Simple wrapper has 'items' + expect(stringListNumber).toBe(1); // Nested wrapper has 'list' + expect(userItemsNumber).toBe(1); // Simple wrapper has 'items' + expect(userListNumber).toBe(1); // Nested wrapper has 'list' + + // Get the generated lock data + const lockData = visitor1.getGeneratedLockData(); + expect(lockData).not.toBeNull(); + + // Verify wrapper types are NOT in lock data (they're auto-generated with deterministic field numbers) + expect(lockData!.messages['ListOfString']).toBeUndefined(); + expect(lockData!.messages['ListOfListOfString']).toBeUndefined(); + expect(lockData!.messages['ListOfUser']).toBeUndefined(); + expect(lockData!.messages['ListOfListOfUser']).toBeUndefined(); + + // Modified schema with some lists removed and new ones added + const modifiedSchema = buildSchema(` + type User { + id: ID! + name: String! + tags: [String] # preserved + # nestedTags: [[String]] # removed + friends: [User] # preserved + friendGroups: [[User]] # preserved + scores: [Int] # new simple nullable list -> ListOfInt + # scoreMatrix: [[Int]] # hypothetical nested list (not added yet) + } + + type Query { + getUsers: [User] + } + `); + + // Create another visitor using the generated lock data + const visitor2 = new GraphQLToProtoTextVisitor(modifiedSchema, { + serviceName: 'UserService', + lockData: lockData || undefined, + }); + + // Generate the second proto + const proto2 = visitor2.visit(); + + // Parse the proto with protobufjs + const root2 = loadProtoFromText(proto2); + + // Verify preserved wrapper types maintain their field numbers + const listOfStringFields2 = getFieldNumbersFromMessage(root2, 'ListOfString'); + const listOfUserFields2 = getFieldNumbersFromMessage(root2, 'ListOfUser'); + const listOfListOfUserFields2 = getFieldNumbersFromMessage(root2, 'ListOfListOfUser'); + const listOfIntFields2 = getFieldNumbersFromMessage(root2, 'ListOfInt'); + + // Verify field numbers are preserved + expect(listOfStringFields2['items']).toBe(stringItemsNumber); + expect(listOfUserFields2['items']).toBe(userItemsNumber); + expect(listOfListOfUserFields2['list']).toBe(userListNumber); + + // Verify new wrapper type has field number 1 + expect(listOfIntFields2['items']).toBe(1); + + // Verify removed wrapper type is not present + // Check if the removed wrapper type exists in the proto + let listOfListOfStringExists = false; + try { + root2.lookupType('ListOfListOfString'); + listOfListOfStringExists = true; + } catch (e) { + // Type doesn't exist, which is expected when the field is removed + listOfListOfStringExists = false; + } + expect(listOfListOfStringExists).toBe(false); // Should not exist since nestedTags was removed + }); + }); }); diff --git a/protographic/tests/sdl-to-proto/11-lists.test.ts b/protographic/tests/sdl-to-proto/11-lists.test.ts new file mode 100644 index 0000000000..ddb3d41d0e --- /dev/null +++ b/protographic/tests/sdl-to-proto/11-lists.test.ts @@ -0,0 +1,2011 @@ +import { describe, expect, it } from 'vitest'; +import { compileGraphQLToProto } from '../../src'; +import { expectValidProto } from '../util'; + +describe('SDL to Proto Lists', () => { + it('should correctly generate protobuf for types with a single non nullable list', () => { + const sdl = ` + type User { + id: ID! + firstName: String! + middleNames: [String]! + lastName: String! + friends: [User!]! + } + + type Query { + getUser: User! + }`; + + const { proto: protoText } = compileGraphQLToProto(sdl); + + expectValidProto(protoText); + + expect(protoText).toMatchInlineSnapshot(` + "syntax = "proto3"; + package service.v1; + + // Service definition for DefaultService + service DefaultService { + rpc QueryGetUser(QueryGetUserRequest) returns (QueryGetUserResponse) {} + } + + // Request message for getUser operation. + message QueryGetUserRequest { + } + // Response message for getUser operation. + message QueryGetUserResponse { + User get_user = 1; + } + + message User { + string id = 1; + string first_name = 2; + repeated string middle_names = 3; + string last_name = 4; + repeated User friends = 5; + }" + `); + }); + + it('should correctly generate protobuf for types with a single nullable list', () => { + const sdl = ` + type User { + id: ID! + firstName: String! + middleNames: [String] + lastName: String! + friends: [User!] + } + + type Query { + getUser: User! + }`; + + const { proto: protoText } = compileGraphQLToProto(sdl); + + expectValidProto(protoText); + + expect(protoText).toMatchInlineSnapshot(` + "syntax = "proto3"; + package service.v1; + + // Service definition for DefaultService + service DefaultService { + rpc QueryGetUser(QueryGetUserRequest) returns (QueryGetUserResponse) {} + } + + // Wrapper message for a list of String. + message ListOfString { + repeated string items = 1; + } + + // Wrapper message for a list of User. + message ListOfUser { + repeated User items = 1; + } + + // Request message for getUser operation. + message QueryGetUserRequest { + } + // Response message for getUser operation. + message QueryGetUserResponse { + User get_user = 1; + } + + message User { + string id = 1; + string first_name = 2; + ListOfString middle_names = 3; + string last_name = 4; + ListOfUser friends = 5; + }" + `); + }); + + it('should correctly generate protobuf for types with a nested non nullable list', () => { + const sdl = ` + type User { + middleNames: [[String]!]! + middleNames2: [[String]]! + friends: [[User!]!]! + } + + type Query { + getUser: User! + }`; + + const { proto: protoText } = compileGraphQLToProto(sdl); + + expectValidProto(protoText); + + expect(protoText).toMatchInlineSnapshot(` + "syntax = "proto3"; + package service.v1; + + // Service definition for DefaultService + service DefaultService { + rpc QueryGetUser(QueryGetUserRequest) returns (QueryGetUserResponse) {} + } + + // Wrapper message for a list of String. + message ListOfListOfString { + message List { + repeated ListOfString items = 1; + } + List list = 1; + } + + // Wrapper message for a list of User. + message ListOfListOfUser { + message List { + repeated ListOfUser items = 1; + } + List list = 1; + } + + // Wrapper message for a list of String. + message ListOfString { + repeated string items = 1; + } + + // Wrapper message for a list of User. + message ListOfUser { + repeated User items = 1; + } + + // Request message for getUser operation. + message QueryGetUserRequest { + } + // Response message for getUser operation. + message QueryGetUserResponse { + User get_user = 1; + } + + message User { + ListOfListOfString middle_names = 1; + ListOfListOfString middle_names_2 = 2; + ListOfListOfUser friends = 3; + }" + `); + }); + + it('should correctly generate protobuf for types with a nested nullable list', () => { + const sdl = ` + type User { + middleNames: [[String]!] + middleNames2: [[String]] + friends: [[User!]!] + } + + type Query { + getUser: User! + }`; + + const { proto: protoText } = compileGraphQLToProto(sdl); + + expectValidProto(protoText); + + expect(protoText).toMatchInlineSnapshot(` + "syntax = "proto3"; + package service.v1; + + // Service definition for DefaultService + service DefaultService { + rpc QueryGetUser(QueryGetUserRequest) returns (QueryGetUserResponse) {} + } + + // Wrapper message for a list of String. + message ListOfListOfString { + message List { + repeated ListOfString items = 1; + } + List list = 1; + } + + // Wrapper message for a list of User. + message ListOfListOfUser { + message List { + repeated ListOfUser items = 1; + } + List list = 1; + } + + // Wrapper message for a list of String. + message ListOfString { + repeated string items = 1; + } + + // Wrapper message for a list of User. + message ListOfUser { + repeated User items = 1; + } + + // Request message for getUser operation. + message QueryGetUserRequest { + } + // Response message for getUser operation. + message QueryGetUserResponse { + User get_user = 1; + } + + message User { + ListOfListOfString middle_names = 1; + ListOfListOfString middle_names_2 = 2; + ListOfListOfUser friends = 3; + }" + `); + }); + + it('should correctly generate protobuf for types with mixed nullable, non nullable, nested and non nested lists', () => { + const sdl = ` + type User { + firstNames: [String]! + lastNames: [String!] + middleNames: [[String]!] + middleNames2: [[String]] + friends: [[User!]!] + friends2: [User!]! + } + + type Query { + getUser: User! + }`; + + const { proto: protoText } = compileGraphQLToProto(sdl); + + expectValidProto(protoText); + + expect(protoText).toMatchInlineSnapshot(` + "syntax = "proto3"; + package service.v1; + + // Service definition for DefaultService + service DefaultService { + rpc QueryGetUser(QueryGetUserRequest) returns (QueryGetUserResponse) {} + } + + // Wrapper message for a list of String. + message ListOfListOfString { + message List { + repeated ListOfString items = 1; + } + List list = 1; + } + + // Wrapper message for a list of User. + message ListOfListOfUser { + message List { + repeated ListOfUser items = 1; + } + List list = 1; + } + + // Wrapper message for a list of String. + message ListOfString { + repeated string items = 1; + } + + // Wrapper message for a list of User. + message ListOfUser { + repeated User items = 1; + } + + // Request message for getUser operation. + message QueryGetUserRequest { + } + // Response message for getUser operation. + message QueryGetUserResponse { + User get_user = 1; + } + + message User { + repeated string first_names = 1; + ListOfString last_names = 2; + ListOfListOfString middle_names = 3; + ListOfListOfString middle_names_2 = 4; + ListOfListOfUser friends = 5; + repeated User friends_2 = 6; + }" + `); + }); + + it('should correctly generate protobuf for lists with enums', () => { + const sdl = ` + enum Status { + ACTIVE + INACTIVE + PENDING + } + + type User { + id: ID! + statuses: [Status!]! + previousStatuses: [Status] + statusHistory: [[Status!]!]! + statusGroups: [[Status]] + } + + type Query { + getUser: User! + }`; + + const { proto: protoText } = compileGraphQLToProto(sdl); + + expectValidProto(protoText); + + expect(protoText).toMatchInlineSnapshot(` + "syntax = "proto3"; + package service.v1; + + // Service definition for DefaultService + service DefaultService { + rpc QueryGetUser(QueryGetUserRequest) returns (QueryGetUserResponse) {} + } + + // Wrapper message for a list of Status. + message ListOfListOfStatus { + message List { + repeated ListOfStatus items = 1; + } + List list = 1; + } + + // Wrapper message for a list of Status. + message ListOfStatus { + repeated Status items = 1; + } + + // Request message for getUser operation. + message QueryGetUserRequest { + } + // Response message for getUser operation. + message QueryGetUserResponse { + User get_user = 1; + } + + message User { + string id = 1; + repeated Status statuses = 2; + ListOfStatus previous_statuses = 3; + ListOfListOfStatus status_history = 4; + ListOfListOfStatus status_groups = 5; + } + + enum Status { + STATUS_UNSPECIFIED = 0; + STATUS_ACTIVE = 1; + STATUS_INACTIVE = 2; + STATUS_PENDING = 3; + }" + `); + }); + + it('should correctly generate protobuf for lists with interfaces', () => { + const sdl = ` + interface Node { + id: ID! + } + + type User implements Node { + id: ID! + name: String! + } + + type Post implements Node { + id: ID! + title: String! + } + + type Timeline { + items: [Node!]! + optionalItems: [Node] + nestedItems: [[Node!]!]! + optionalNestedItems: [[Node]] + } + + type Query { + getTimeline: Timeline! + }`; + + const { proto: protoText } = compileGraphQLToProto(sdl); + + expectValidProto(protoText); + + expect(protoText).toMatchInlineSnapshot(` + "syntax = "proto3"; + package service.v1; + + // Service definition for DefaultService + service DefaultService { + rpc QueryGetTimeline(QueryGetTimelineRequest) returns (QueryGetTimelineResponse) {} + } + + // Wrapper message for a list of Node. + message ListOfListOfNode { + message List { + repeated ListOfNode items = 1; + } + List list = 1; + } + + // Wrapper message for a list of Node. + message ListOfNode { + repeated Node items = 1; + } + + // Request message for getTimeline operation. + message QueryGetTimelineRequest { + } + // Response message for getTimeline operation. + message QueryGetTimelineResponse { + Timeline get_timeline = 1; + } + + message Timeline { + repeated Node items = 1; + ListOfNode optional_items = 2; + ListOfListOfNode nested_items = 3; + ListOfListOfNode optional_nested_items = 4; + } + + message Node { + oneof instance { + User user = 1; + Post post = 2; + } + } + + message User { + string id = 1; + string name = 2; + } + + message Post { + string id = 1; + string title = 2; + }" + `); + }); + + it('should correctly generate protobuf for lists with unions', () => { + const sdl = ` + type User { + id: ID! + name: String! + } + + type Post { + id: ID! + title: String! + } + + union SearchResult = User | Post + + type SearchResults { + results: [SearchResult!]! + optionalResults: [SearchResult] + nestedResults: [[SearchResult!]!]! + optionalNestedResults: [[SearchResult]] + } + + type Query { + search: SearchResults! + }`; + + const { proto: protoText } = compileGraphQLToProto(sdl); + + expectValidProto(protoText); + + expect(protoText).toMatchInlineSnapshot(` + "syntax = "proto3"; + package service.v1; + + // Service definition for DefaultService + service DefaultService { + rpc QuerySearch(QuerySearchRequest) returns (QuerySearchResponse) {} + } + + // Wrapper message for a list of SearchResult. + message ListOfListOfSearchResult { + message List { + repeated ListOfSearchResult items = 1; + } + List list = 1; + } + + // Wrapper message for a list of SearchResult. + message ListOfSearchResult { + repeated SearchResult items = 1; + } + + // Request message for search operation. + message QuerySearchRequest { + } + // Response message for search operation. + message QuerySearchResponse { + SearchResults search = 1; + } + + message SearchResults { + repeated SearchResult results = 1; + ListOfSearchResult optional_results = 2; + ListOfListOfSearchResult nested_results = 3; + ListOfListOfSearchResult optional_nested_results = 4; + } + + message User { + string id = 1; + string name = 2; + } + + message Post { + string id = 1; + string title = 2; + } + + message SearchResult { + oneof value { + User user = 1; + Post post = 2; + } + }" + `); + }); + + it('should correctly generate protobuf for lists with scalars and custom scalars', () => { + const sdl = ` + scalar DateTime + scalar JSON + + type User { + id: ID! + tags: [String!]! + optionalTags: [String] + scores: [Int!]! + optionalScores: [Int] + ratings: [Float!]! + optionalRatings: [Float] + timestamps: [DateTime!]! + optionalTimestamps: [DateTime] + metadata: [JSON!]! + optionalMetadata: [JSON] + nestedTags: [[String!]!]! + nestedOptionalTags: [[String]] + } + + type Query { + getUser: User! + }`; + + const { proto: protoText } = compileGraphQLToProto(sdl); + + expectValidProto(protoText); + + expect(protoText).toMatchInlineSnapshot(` + "syntax = "proto3"; + package service.v1; + + // Service definition for DefaultService + service DefaultService { + rpc QueryGetUser(QueryGetUserRequest) returns (QueryGetUserResponse) {} + } + + // Wrapper message for a list of DateTime. + message ListOfDateTime { + repeated string items = 1; + } + + // Wrapper message for a list of Float. + message ListOfFloat { + repeated double items = 1; + } + + // Wrapper message for a list of Int. + message ListOfInt { + repeated int32 items = 1; + } + + // Wrapper message for a list of JSON. + message ListOfJSON { + repeated string items = 1; + } + + // Wrapper message for a list of String. + message ListOfListOfString { + message List { + repeated ListOfString items = 1; + } + List list = 1; + } + + // Wrapper message for a list of String. + message ListOfString { + repeated string items = 1; + } + + // Request message for getUser operation. + message QueryGetUserRequest { + } + // Response message for getUser operation. + message QueryGetUserResponse { + User get_user = 1; + } + + message User { + string id = 1; + repeated string tags = 2; + ListOfString optional_tags = 3; + repeated int32 scores = 4; + ListOfInt optional_scores = 5; + repeated double ratings = 6; + ListOfFloat optional_ratings = 7; + repeated string timestamps = 8; + ListOfDateTime optional_timestamps = 9; + repeated string metadata = 10; + ListOfJSON optional_metadata = 11; + ListOfListOfString nested_tags = 12; + ListOfListOfString nested_optional_tags = 13; + }" + `); + }); + + it('should correctly generate protobuf for deeply nested lists', () => { + const sdl = ` + type User { + id: ID! + name: String! + } + + type Matrix { + level1: [[[String!]!]!]! + level2: [[[User!]!]!]! + level3: [[[[String]]]] + level4: [[[[[User!]!]!]!]!]! + } + + type Query { + getMatrix: Matrix! + }`; + + const { proto: protoText } = compileGraphQLToProto(sdl); + + expectValidProto(protoText); + + expect(protoText).toMatchInlineSnapshot(` + "syntax = "proto3"; + package service.v1; + + // Service definition for DefaultService + service DefaultService { + rpc QueryGetMatrix(QueryGetMatrixRequest) returns (QueryGetMatrixResponse) {} + } + + // Wrapper message for a list of User. + message ListOfListOfListOfListOfListOfUser { + message List { + repeated ListOfListOfListOfListOfUser items = 1; + } + List list = 1; + } + + // Wrapper message for a list of String. + message ListOfListOfListOfListOfString { + message List { + repeated ListOfListOfListOfString items = 1; + } + List list = 1; + } + + // Wrapper message for a list of User. + message ListOfListOfListOfListOfUser { + message List { + repeated ListOfListOfListOfUser items = 1; + } + List list = 1; + } + + // Wrapper message for a list of String. + message ListOfListOfListOfString { + message List { + repeated ListOfListOfString items = 1; + } + List list = 1; + } + + // Wrapper message for a list of User. + message ListOfListOfListOfUser { + message List { + repeated ListOfListOfUser items = 1; + } + List list = 1; + } + + // Wrapper message for a list of String. + message ListOfListOfString { + message List { + repeated ListOfString items = 1; + } + List list = 1; + } + + // Wrapper message for a list of User. + message ListOfListOfUser { + message List { + repeated ListOfUser items = 1; + } + List list = 1; + } + + // Wrapper message for a list of String. + message ListOfString { + repeated string items = 1; + } + + // Wrapper message for a list of User. + message ListOfUser { + repeated User items = 1; + } + + // Request message for getMatrix operation. + message QueryGetMatrixRequest { + } + // Response message for getMatrix operation. + message QueryGetMatrixResponse { + Matrix get_matrix = 1; + } + + message Matrix { + ListOfListOfListOfString level_1 = 1; + ListOfListOfListOfUser level_2 = 2; + ListOfListOfListOfListOfString level_3 = 3; + ListOfListOfListOfListOfListOfUser level_4 = 4; + } + + message User { + string id = 1; + string name = 2; + }" + `); + }); + + it('should correctly generate protobuf for lists with input types in mutations', () => { + const sdl = ` + input CreateUserInput { + name: String! + tags: [String!]! + optionalTags: [String] + } + + input UpdateUserInput { + id: ID! + name: String + addTags: [String!] + removeTags: [String] + } + + type User { + id: ID! + name: String! + tags: [String!]! + } + + type Mutation { + createUser(input: CreateUserInput!): User! + createUsers(inputs: [CreateUserInput!]!): [User!]! + updateUsers(inputs: [UpdateUserInput!]!): [User!]! + bulkUpdate(updates: [[UpdateUserInput!]!]!): [User!]! + } + + type Query { + getUser: User! + }`; + + const { proto: protoText } = compileGraphQLToProto(sdl); + + expectValidProto(protoText); + + expect(protoText).toMatchInlineSnapshot(` + "syntax = "proto3"; + package service.v1; + + import "google/protobuf/wrappers.proto"; + + // Service definition for DefaultService + service DefaultService { + rpc MutationBulkUpdate(MutationBulkUpdateRequest) returns (MutationBulkUpdateResponse) {} + rpc MutationCreateUser(MutationCreateUserRequest) returns (MutationCreateUserResponse) {} + rpc MutationCreateUsers(MutationCreateUsersRequest) returns (MutationCreateUsersResponse) {} + rpc MutationUpdateUsers(MutationUpdateUsersRequest) returns (MutationUpdateUsersResponse) {} + rpc QueryGetUser(QueryGetUserRequest) returns (QueryGetUserResponse) {} + } + + // Wrapper message for a list of UpdateUserInput. + message ListOfListOfUpdateUserInput { + message List { + repeated ListOfUpdateUserInput items = 1; + } + List list = 1; + } + + // Wrapper message for a list of String. + message ListOfString { + repeated string items = 1; + } + + // Wrapper message for a list of UpdateUserInput. + message ListOfUpdateUserInput { + repeated UpdateUserInput items = 1; + } + + // Request message for getUser operation. + message QueryGetUserRequest { + } + // Response message for getUser operation. + message QueryGetUserResponse { + User get_user = 1; + } + // Request message for createUser operation. + message MutationCreateUserRequest { + CreateUserInput input = 1; + } + // Response message for createUser operation. + message MutationCreateUserResponse { + User create_user = 1; + } + // Request message for createUsers operation. + message MutationCreateUsersRequest { + repeated CreateUserInput inputs = 1; + } + // Response message for createUsers operation. + message MutationCreateUsersResponse { + repeated User create_users = 1; + } + // Request message for updateUsers operation. + message MutationUpdateUsersRequest { + repeated UpdateUserInput inputs = 1; + } + // Response message for updateUsers operation. + message MutationUpdateUsersResponse { + repeated User update_users = 1; + } + // Request message for bulkUpdate operation. + message MutationBulkUpdateRequest { + ListOfListOfUpdateUserInput updates = 1; + } + // Response message for bulkUpdate operation. + message MutationBulkUpdateResponse { + repeated User bulk_update = 1; + } + + message User { + string id = 1; + string name = 2; + repeated string tags = 3; + } + + message CreateUserInput { + string name = 1; + repeated string tags = 2; + ListOfString optional_tags = 3; + } + + message UpdateUserInput { + string id = 1; + google.protobuf.StringValue name = 2; + ListOfString add_tags = 3; + ListOfString remove_tags = 4; + }" + `); + }); + + it('should correctly generate protobuf for mixed complex lists with all types', () => { + const sdl = ` + enum Priority { + LOW + MEDIUM + HIGH + } + + scalar DateTime + + interface Node { + id: ID! + } + + type User implements Node { + id: ID! + name: String! + } + + type Task implements Node { + id: ID! + title: String! + } + + union Item = User | Task + + input FilterInput { + priorities: [Priority!] + userIds: [ID] + } + + type ComplexType { + # Simple lists + strings: [String!]! + optionalStrings: [String] + + # Enum lists + priorities: [Priority!]! + optionalPriorities: [Priority] + + # Interface lists + nodes: [Node!]! + optionalNodes: [Node] + + # Union lists + items: [Item!]! + optionalItems: [Item] + + # Nested lists + nestedStrings: [[String!]!]! + nestedOptionalStrings: [[String]] + nestedPriorities: [[Priority!]!]! + nestedOptionalPriorities: [[Priority]] + + # Complex nested + deepNestedItems: [[[Item!]!]!]! + mixedNested: [[[[Priority]]]] + } + + type Query { + getComplex(filter: FilterInput): ComplexType! + }`; + + const { proto: protoText } = compileGraphQLToProto(sdl); + + expectValidProto(protoText); + + expect(protoText).toMatchInlineSnapshot(` + "syntax = "proto3"; + package service.v1; + + // Service definition for DefaultService + service DefaultService { + rpc QueryGetComplex(QueryGetComplexRequest) returns (QueryGetComplexResponse) {} + } + + // Wrapper message for a list of ID. + message ListOfID { + repeated string items = 1; + } + + // Wrapper message for a list of Item. + message ListOfItem { + repeated Item items = 1; + } + + // Wrapper message for a list of Item. + message ListOfListOfItem { + message List { + repeated ListOfItem items = 1; + } + List list = 1; + } + + // Wrapper message for a list of Item. + message ListOfListOfListOfItem { + message List { + repeated ListOfListOfItem items = 1; + } + List list = 1; + } + + // Wrapper message for a list of Priority. + message ListOfListOfListOfListOfPriority { + message List { + repeated ListOfListOfListOfPriority items = 1; + } + List list = 1; + } + + // Wrapper message for a list of Priority. + message ListOfListOfListOfPriority { + message List { + repeated ListOfListOfPriority items = 1; + } + List list = 1; + } + + // Wrapper message for a list of Priority. + message ListOfListOfPriority { + message List { + repeated ListOfPriority items = 1; + } + List list = 1; + } + + // Wrapper message for a list of String. + message ListOfListOfString { + message List { + repeated ListOfString items = 1; + } + List list = 1; + } + + // Wrapper message for a list of Node. + message ListOfNode { + repeated Node items = 1; + } + + // Wrapper message for a list of Priority. + message ListOfPriority { + repeated Priority items = 1; + } + + // Wrapper message for a list of String. + message ListOfString { + repeated string items = 1; + } + + // Request message for getComplex operation. + message QueryGetComplexRequest { + FilterInput filter = 1; + } + // Response message for getComplex operation. + message QueryGetComplexResponse { + ComplexType get_complex = 1; + } + + message FilterInput { + ListOfPriority priorities = 1; + ListOfID user_ids = 2; + } + + message ComplexType { + repeated string strings = 1; + ListOfString optional_strings = 2; + repeated Priority priorities = 3; + ListOfPriority optional_priorities = 4; + repeated Node nodes = 5; + ListOfNode optional_nodes = 6; + repeated Item items = 7; + ListOfItem optional_items = 8; + ListOfListOfString nested_strings = 9; + ListOfListOfString nested_optional_strings = 10; + ListOfListOfPriority nested_priorities = 11; + ListOfListOfPriority nested_optional_priorities = 12; + ListOfListOfListOfItem deep_nested_items = 13; + ListOfListOfListOfListOfPriority mixed_nested = 14; + } + + enum Priority { + PRIORITY_UNSPECIFIED = 0; + PRIORITY_LOW = 1; + PRIORITY_MEDIUM = 2; + PRIORITY_HIGH = 3; + } + + message Node { + oneof instance { + User user = 1; + Task task = 2; + } + } + + message User { + string id = 1; + string name = 2; + } + + message Task { + string id = 1; + string title = 2; + } + + message Item { + oneof value { + User user = 1; + Task task = 2; + } + }" + `); + }); + + it('should correctly generate protobuf for edge cases with empty lists and optional nesting', () => { + const sdl = ` + type User { + id: ID! + } + + type EdgeCases { + # Various nullable combinations + case1: [String!] + case2: [String]! + case3: [String] + + # Nested nullable combinations + case4: [[String!]!] + case5: [[String!]]! + case6: [[String!]] + case7: [[String]!]! + case8: [[String]!] + case9: [[String]]! + case10: [[String]] + + # With objects + users1: [User!] + users2: [User]! + users3: [User] + nestedUsers1: [[User!]!] + nestedUsers2: [[User!]] + nestedUsers3: [[User]!] + nestedUsers4: [[User]] + } + + type Query { + getEdgeCases: EdgeCases! + }`; + + const { proto: protoText } = compileGraphQLToProto(sdl); + + expectValidProto(protoText); + + expect(protoText).toMatchInlineSnapshot(` + "syntax = "proto3"; + package service.v1; + + // Service definition for DefaultService + service DefaultService { + rpc QueryGetEdgeCases(QueryGetEdgeCasesRequest) returns (QueryGetEdgeCasesResponse) {} + } + + // Wrapper message for a list of String. + message ListOfListOfString { + message List { + repeated ListOfString items = 1; + } + List list = 1; + } + + // Wrapper message for a list of User. + message ListOfListOfUser { + message List { + repeated ListOfUser items = 1; + } + List list = 1; + } + + // Wrapper message for a list of String. + message ListOfString { + repeated string items = 1; + } + + // Wrapper message for a list of User. + message ListOfUser { + repeated User items = 1; + } + + // Request message for getEdgeCases operation. + message QueryGetEdgeCasesRequest { + } + // Response message for getEdgeCases operation. + message QueryGetEdgeCasesResponse { + EdgeCases get_edge_cases = 1; + } + + message EdgeCases { + ListOfString case_1 = 1; + repeated string case_2 = 2; + ListOfString case_3 = 3; + ListOfListOfString case_4 = 4; + ListOfListOfString case_5 = 5; + ListOfListOfString case_6 = 6; + ListOfListOfString case_7 = 7; + ListOfListOfString case_8 = 8; + ListOfListOfString case_9 = 9; + ListOfListOfString case_10 = 10; + ListOfUser users_1 = 11; + repeated User users_2 = 12; + ListOfUser users_3 = 13; + ListOfListOfUser nested_users_1 = 14; + ListOfListOfUser nested_users_2 = 15; + ListOfListOfUser nested_users_3 = 16; + ListOfListOfUser nested_users_4 = 17; + } + + message User { + string id = 1; + }" + `); + }); + + it('should correctly generate protobuf for recursive types with lists', () => { + const sdl = ` + type User { + id: ID! + name: String! + friends: [User!]! + optionalFriends: [User] + nestedFriendGroups: [[User!]!]! + optionalNestedFriendGroups: [[User]] + } + + type Comment { + id: ID! + content: String! + author: User! + replies: [Comment!]! + optionalReplies: [Comment] + nestedReplies: [[Comment!]!]! + } + + type Category { + id: ID! + name: String! + parent: Category + children: [Category!]! + optionalChildren: [Category] + subCategories: [[Category!]!]! + relatedCategories: [Category] + } + + type Query { + getUser: User! + getComment: Comment! + getCategory: Category! + }`; + + const { proto: protoText } = compileGraphQLToProto(sdl); + + expectValidProto(protoText); + + expect(protoText).toMatchInlineSnapshot(` + "syntax = "proto3"; + package service.v1; + + // Service definition for DefaultService + service DefaultService { + rpc QueryGetCategory(QueryGetCategoryRequest) returns (QueryGetCategoryResponse) {} + rpc QueryGetComment(QueryGetCommentRequest) returns (QueryGetCommentResponse) {} + rpc QueryGetUser(QueryGetUserRequest) returns (QueryGetUserResponse) {} + } + + // Wrapper message for a list of Category. + message ListOfCategory { + repeated Category items = 1; + } + + // Wrapper message for a list of Comment. + message ListOfComment { + repeated Comment items = 1; + } + + // Wrapper message for a list of Category. + message ListOfListOfCategory { + message List { + repeated ListOfCategory items = 1; + } + List list = 1; + } + + // Wrapper message for a list of Comment. + message ListOfListOfComment { + message List { + repeated ListOfComment items = 1; + } + List list = 1; + } + + // Wrapper message for a list of User. + message ListOfListOfUser { + message List { + repeated ListOfUser items = 1; + } + List list = 1; + } + + // Wrapper message for a list of User. + message ListOfUser { + repeated User items = 1; + } + + // Request message for getUser operation. + message QueryGetUserRequest { + } + // Response message for getUser operation. + message QueryGetUserResponse { + User get_user = 1; + } + // Request message for getComment operation. + message QueryGetCommentRequest { + } + // Response message for getComment operation. + message QueryGetCommentResponse { + Comment get_comment = 1; + } + // Request message for getCategory operation. + message QueryGetCategoryRequest { + } + // Response message for getCategory operation. + message QueryGetCategoryResponse { + Category get_category = 1; + } + + message User { + string id = 1; + string name = 2; + repeated User friends = 3; + ListOfUser optional_friends = 4; + ListOfListOfUser nested_friend_groups = 5; + ListOfListOfUser optional_nested_friend_groups = 6; + } + + message Comment { + string id = 1; + string content = 2; + User author = 3; + repeated Comment replies = 4; + ListOfComment optional_replies = 5; + ListOfListOfComment nested_replies = 6; + } + + message Category { + string id = 1; + string name = 2; + Category parent = 3; + repeated Category children = 4; + ListOfCategory optional_children = 5; + ListOfListOfCategory sub_categories = 6; + ListOfCategory related_categories = 7; + }" + `); + }); + + it('should correctly generate protobuf for complex input types with lists', () => { + const sdl = ` + input TagInput { + name: String! + weight: Float + } + + input UserFilterInput { + ids: [ID!] + optionalIds: [ID] + tags: [TagInput!]! + optionalTags: [TagInput] + nestedTags: [[TagInput!]!]! + optionalNestedTags: [[TagInput]] + names: [String!] + scores: [Int] + ratings: [Float!] + } + + input SortInput { + field: String! + direction: String! + } + + input PaginationInput { + limit: Int + offset: Int + } + + input SearchInput { + query: String! + filters: [UserFilterInput!]! + optionalFilters: [UserFilterInput] + nestedFilters: [[UserFilterInput!]!]! + sorts: [SortInput!] + pagination: PaginationInput + } + + type User { + id: ID! + name: String! + tags: [String!]! + } + + type SearchResult { + users: [User!]! + total: Int! + } + + type Query { + searchUsers(input: SearchInput!): SearchResult! + } + + type Mutation { + updateUsers(updates: [UserFilterInput!]!): [User!]! + bulkSearch(searches: [SearchInput!]!): [SearchResult!]! + }`; + + const { proto: protoText } = compileGraphQLToProto(sdl); + + expectValidProto(protoText); + + expect(protoText).toMatchInlineSnapshot(` + "syntax = "proto3"; + package service.v1; + + import "google/protobuf/wrappers.proto"; + + // Service definition for DefaultService + service DefaultService { + rpc MutationBulkSearch(MutationBulkSearchRequest) returns (MutationBulkSearchResponse) {} + rpc MutationUpdateUsers(MutationUpdateUsersRequest) returns (MutationUpdateUsersResponse) {} + rpc QuerySearchUsers(QuerySearchUsersRequest) returns (QuerySearchUsersResponse) {} + } + + // Wrapper message for a list of Float. + message ListOfFloat { + repeated double items = 1; + } + + // Wrapper message for a list of ID. + message ListOfID { + repeated string items = 1; + } + + // Wrapper message for a list of Int. + message ListOfInt { + repeated int32 items = 1; + } + + // Wrapper message for a list of TagInput. + message ListOfListOfTagInput { + message List { + repeated ListOfTagInput items = 1; + } + List list = 1; + } + + // Wrapper message for a list of UserFilterInput. + message ListOfListOfUserFilterInput { + message List { + repeated ListOfUserFilterInput items = 1; + } + List list = 1; + } + + // Wrapper message for a list of SortInput. + message ListOfSortInput { + repeated SortInput items = 1; + } + + // Wrapper message for a list of String. + message ListOfString { + repeated string items = 1; + } + + // Wrapper message for a list of TagInput. + message ListOfTagInput { + repeated TagInput items = 1; + } + + // Wrapper message for a list of UserFilterInput. + message ListOfUserFilterInput { + repeated UserFilterInput items = 1; + } + + // Request message for searchUsers operation. + message QuerySearchUsersRequest { + SearchInput input = 1; + } + // Response message for searchUsers operation. + message QuerySearchUsersResponse { + SearchResult search_users = 1; + } + // Request message for updateUsers operation. + message MutationUpdateUsersRequest { + repeated UserFilterInput updates = 1; + } + // Response message for updateUsers operation. + message MutationUpdateUsersResponse { + repeated User update_users = 1; + } + // Request message for bulkSearch operation. + message MutationBulkSearchRequest { + repeated SearchInput searches = 1; + } + // Response message for bulkSearch operation. + message MutationBulkSearchResponse { + repeated SearchResult bulk_search = 1; + } + + message SearchInput { + string query = 1; + repeated UserFilterInput filters = 2; + ListOfUserFilterInput optional_filters = 3; + ListOfListOfUserFilterInput nested_filters = 4; + ListOfSortInput sorts = 5; + PaginationInput pagination = 6; + } + + message SearchResult { + repeated User users = 1; + int32 total = 2; + } + + message UserFilterInput { + ListOfID ids = 1; + ListOfID optional_ids = 2; + repeated TagInput tags = 3; + ListOfTagInput optional_tags = 4; + ListOfListOfTagInput nested_tags = 5; + ListOfListOfTagInput optional_nested_tags = 6; + ListOfString names = 7; + ListOfInt scores = 8; + ListOfFloat ratings = 9; + } + + message User { + string id = 1; + string name = 2; + repeated string tags = 3; + } + + message TagInput { + string name = 1; + google.protobuf.DoubleValue weight = 2; + } + + message SortInput { + string field = 1; + string direction = 2; + } + + message PaginationInput { + google.protobuf.Int32Value limit = 1; + google.protobuf.Int32Value offset = 2; + }" + `); + }); + + it('should correctly generate protobuf for recursive input types', () => { + const sdl = ` + input CategoryInput { + id: ID! + name: String! + parentId: ID + children: [CategoryInput!] + optionalChildren: [CategoryInput] + nestedChildren: [[CategoryInput!]!] + } + + input CommentInput { + id: ID! + content: String! + authorId: ID! + replies: [CommentInput!]! + optionalReplies: [CommentInput] + nestedReplies: [[CommentInput!]!]! + } + + input FilterNodeInput { + field: String! + value: String! + operator: String! + children: [FilterNodeInput!] + optionalChildren: [FilterNodeInput] + andConditions: [[FilterNodeInput!]!] + orConditions: [[FilterNodeInput]] + } + + type Category { + id: ID! + name: String! + } + + type Comment { + id: ID! + content: String! + } + + type FilterResult { + matched: Boolean! + count: Int! + } + + type Query { + getCategory: Category! + } + + type Mutation { + createCategory(input: CategoryInput!): Category! + createCategories(inputs: [CategoryInput!]!): [Category!]! + createComment(input: CommentInput!): Comment! + createComments(inputs: [CommentInput!]!): [Comment!]! + applyFilter(filter: FilterNodeInput!): FilterResult! + applyFilters(filters: [FilterNodeInput!]!): [FilterResult!]! + complexFilter(filters: [[FilterNodeInput!]!]!): FilterResult! + }`; + + const { proto: protoText } = compileGraphQLToProto(sdl); + + expectValidProto(protoText); + + expect(protoText).toMatchInlineSnapshot(` + "syntax = "proto3"; + package service.v1; + + import "google/protobuf/wrappers.proto"; + + // Service definition for DefaultService + service DefaultService { + rpc MutationApplyFilter(MutationApplyFilterRequest) returns (MutationApplyFilterResponse) {} + rpc MutationApplyFilters(MutationApplyFiltersRequest) returns (MutationApplyFiltersResponse) {} + rpc MutationComplexFilter(MutationComplexFilterRequest) returns (MutationComplexFilterResponse) {} + rpc MutationCreateCategories(MutationCreateCategoriesRequest) returns (MutationCreateCategoriesResponse) {} + rpc MutationCreateCategory(MutationCreateCategoryRequest) returns (MutationCreateCategoryResponse) {} + rpc MutationCreateComment(MutationCreateCommentRequest) returns (MutationCreateCommentResponse) {} + rpc MutationCreateComments(MutationCreateCommentsRequest) returns (MutationCreateCommentsResponse) {} + rpc QueryGetCategory(QueryGetCategoryRequest) returns (QueryGetCategoryResponse) {} + } + + // Wrapper message for a list of CategoryInput. + message ListOfCategoryInput { + repeated CategoryInput items = 1; + } + + // Wrapper message for a list of CommentInput. + message ListOfCommentInput { + repeated CommentInput items = 1; + } + + // Wrapper message for a list of FilterNodeInput. + message ListOfFilterNodeInput { + repeated FilterNodeInput items = 1; + } + + // Wrapper message for a list of CategoryInput. + message ListOfListOfCategoryInput { + message List { + repeated ListOfCategoryInput items = 1; + } + List list = 1; + } + + // Wrapper message for a list of CommentInput. + message ListOfListOfCommentInput { + message List { + repeated ListOfCommentInput items = 1; + } + List list = 1; + } + + // Wrapper message for a list of FilterNodeInput. + message ListOfListOfFilterNodeInput { + message List { + repeated ListOfFilterNodeInput items = 1; + } + List list = 1; + } + + // Request message for getCategory operation. + message QueryGetCategoryRequest { + } + // Response message for getCategory operation. + message QueryGetCategoryResponse { + Category get_category = 1; + } + // Request message for createCategory operation. + message MutationCreateCategoryRequest { + CategoryInput input = 1; + } + // Response message for createCategory operation. + message MutationCreateCategoryResponse { + Category create_category = 1; + } + // Request message for createCategories operation. + message MutationCreateCategoriesRequest { + repeated CategoryInput inputs = 1; + } + // Response message for createCategories operation. + message MutationCreateCategoriesResponse { + repeated Category create_categories = 1; + } + // Request message for createComment operation. + message MutationCreateCommentRequest { + CommentInput input = 1; + } + // Response message for createComment operation. + message MutationCreateCommentResponse { + Comment create_comment = 1; + } + // Request message for createComments operation. + message MutationCreateCommentsRequest { + repeated CommentInput inputs = 1; + } + // Response message for createComments operation. + message MutationCreateCommentsResponse { + repeated Comment create_comments = 1; + } + // Request message for applyFilter operation. + message MutationApplyFilterRequest { + FilterNodeInput filter = 1; + } + // Response message for applyFilter operation. + message MutationApplyFilterResponse { + FilterResult apply_filter = 1; + } + // Request message for applyFilters operation. + message MutationApplyFiltersRequest { + repeated FilterNodeInput filters = 1; + } + // Response message for applyFilters operation. + message MutationApplyFiltersResponse { + repeated FilterResult apply_filters = 1; + } + // Request message for complexFilter operation. + message MutationComplexFilterRequest { + ListOfListOfFilterNodeInput filters = 1; + } + // Response message for complexFilter operation. + message MutationComplexFilterResponse { + FilterResult complex_filter = 1; + } + + message Category { + string id = 1; + string name = 2; + } + + message CategoryInput { + string id = 1; + string name = 2; + google.protobuf.StringValue parent_id = 3; + ListOfCategoryInput children = 4; + ListOfCategoryInput optional_children = 5; + ListOfListOfCategoryInput nested_children = 6; + } + + message CommentInput { + string id = 1; + string content = 2; + string author_id = 3; + repeated CommentInput replies = 4; + ListOfCommentInput optional_replies = 5; + ListOfListOfCommentInput nested_replies = 6; + } + + message Comment { + string id = 1; + string content = 2; + } + + message FilterNodeInput { + string field = 1; + string value = 2; + string operator = 3; + ListOfFilterNodeInput children = 4; + ListOfFilterNodeInput optional_children = 5; + ListOfListOfFilterNodeInput and_conditions = 6; + ListOfListOfFilterNodeInput or_conditions = 7; + } + + message FilterResult { + bool matched = 1; + int32 count = 2; + }" + `); + }); + + it('should correctly generate protobuf for mixed recursive and non-recursive types with complex list nesting', () => { + const sdl = ` + enum Status { + ACTIVE + INACTIVE + PENDING + } + + scalar DateTime + + input MetadataInput { + key: String! + value: String! + tags: [String!] + nestedData: [MetadataInput] + } + + input TreeNodeInput { + id: ID! + value: String! + status: Status! + metadata: [MetadataInput!] + children: [TreeNodeInput!] + optionalChildren: [TreeNodeInput] + nestedChildren: [[TreeNodeInput!]!] + siblingGroups: [[[TreeNodeInput]]] + } + + type TreeNode { + id: ID! + value: String! + status: Status! + children: [TreeNode!]! + optionalChildren: [TreeNode] + nestedChildren: [[TreeNode!]!]! + parent: TreeNode + ancestors: [TreeNode!]! + descendants: [[TreeNode]] + } + + type ProcessingResult { + nodes: [TreeNode!]! + errors: [String] + warnings: [[String]] + metadata: [String!]! + } + + type Query { + getTree: TreeNode! + } + + type Mutation { + processTree(input: TreeNodeInput!): ProcessingResult! + processTrees(inputs: [TreeNodeInput!]!): [ProcessingResult!]! + bulkProcess(batches: [[TreeNodeInput!]!]!): ProcessingResult! + complexProcess(data: [[[TreeNodeInput]]]!): [ProcessingResult] + }`; + + const { proto: protoText } = compileGraphQLToProto(sdl); + + expectValidProto(protoText); + + expect(protoText).toMatchInlineSnapshot(` + "syntax = "proto3"; + package service.v1; + + // Service definition for DefaultService + service DefaultService { + rpc MutationBulkProcess(MutationBulkProcessRequest) returns (MutationBulkProcessResponse) {} + rpc MutationComplexProcess(MutationComplexProcessRequest) returns (MutationComplexProcessResponse) {} + rpc MutationProcessTree(MutationProcessTreeRequest) returns (MutationProcessTreeResponse) {} + rpc MutationProcessTrees(MutationProcessTreesRequest) returns (MutationProcessTreesResponse) {} + rpc QueryGetTree(QueryGetTreeRequest) returns (QueryGetTreeResponse) {} + } + + // Wrapper message for a list of TreeNodeInput. + message ListOfListOfListOfTreeNodeInput { + message List { + repeated ListOfListOfTreeNodeInput items = 1; + } + List list = 1; + } + + // Wrapper message for a list of String. + message ListOfListOfString { + message List { + repeated ListOfString items = 1; + } + List list = 1; + } + + // Wrapper message for a list of TreeNode. + message ListOfListOfTreeNode { + message List { + repeated ListOfTreeNode items = 1; + } + List list = 1; + } + + // Wrapper message for a list of TreeNodeInput. + message ListOfListOfTreeNodeInput { + message List { + repeated ListOfTreeNodeInput items = 1; + } + List list = 1; + } + + // Wrapper message for a list of MetadataInput. + message ListOfMetadataInput { + repeated MetadataInput items = 1; + } + + // Wrapper message for a list of ProcessingResult. + message ListOfProcessingResult { + repeated ProcessingResult items = 1; + } + + // Wrapper message for a list of String. + message ListOfString { + repeated string items = 1; + } + + // Wrapper message for a list of TreeNode. + message ListOfTreeNode { + repeated TreeNode items = 1; + } + + // Wrapper message for a list of TreeNodeInput. + message ListOfTreeNodeInput { + repeated TreeNodeInput items = 1; + } + + // Request message for getTree operation. + message QueryGetTreeRequest { + } + // Response message for getTree operation. + message QueryGetTreeResponse { + TreeNode get_tree = 1; + } + // Request message for processTree operation. + message MutationProcessTreeRequest { + TreeNodeInput input = 1; + } + // Response message for processTree operation. + message MutationProcessTreeResponse { + ProcessingResult process_tree = 1; + } + // Request message for processTrees operation. + message MutationProcessTreesRequest { + repeated TreeNodeInput inputs = 1; + } + // Response message for processTrees operation. + message MutationProcessTreesResponse { + repeated ProcessingResult process_trees = 1; + } + // Request message for bulkProcess operation. + message MutationBulkProcessRequest { + ListOfListOfTreeNodeInput batches = 1; + } + // Response message for bulkProcess operation. + message MutationBulkProcessResponse { + ProcessingResult bulk_process = 1; + } + // Request message for complexProcess operation. + message MutationComplexProcessRequest { + ListOfListOfListOfTreeNodeInput data = 1; + } + // Response message for complexProcess operation. + message MutationComplexProcessResponse { + ListOfProcessingResult complex_process = 1; + } + + message TreeNode { + string id = 1; + string value = 2; + Status status = 3; + repeated TreeNode children = 4; + ListOfTreeNode optional_children = 5; + ListOfListOfTreeNode nested_children = 6; + TreeNode parent = 7; + repeated TreeNode ancestors = 8; + ListOfListOfTreeNode descendants = 9; + } + + message TreeNodeInput { + string id = 1; + string value = 2; + Status status = 3; + ListOfMetadataInput metadata = 4; + ListOfTreeNodeInput children = 5; + ListOfTreeNodeInput optional_children = 6; + ListOfListOfTreeNodeInput nested_children = 7; + ListOfListOfListOfTreeNodeInput sibling_groups = 8; + } + + message ProcessingResult { + repeated TreeNode nodes = 1; + ListOfString errors = 2; + ListOfListOfString warnings = 3; + repeated string metadata = 4; + } + + enum Status { + STATUS_UNSPECIFIED = 0; + STATUS_ACTIVE = 1; + STATUS_INACTIVE = 2; + STATUS_PENDING = 3; + } + + message MetadataInput { + string key = 1; + string value = 2; + ListOfString tags = 3; + ListOfMetadataInput nested_data = 4; + }" + `); + }); +}); diff --git a/vitest.workspace.ts b/vitest.workspace.ts index b664ff164c..8f75c72b4b 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -1 +1 @@ -export default ['controlplane', 'shared', 'cli', 'composition']; +export default ['controlplane', 'shared', 'cli', 'composition', 'protographic'];