diff --git a/protographic/src/sdl-to-proto-visitor.ts b/protographic/src/sdl-to-proto-visitor.ts index 108a942d18..2532abf11f 100644 --- a/protographic/src/sdl-to-proto-visitor.ts +++ b/protographic/src/sdl-to-proto-visitor.ts @@ -96,6 +96,14 @@ interface ProtoType { isRepeated: boolean; } +/** + * Data structure for key directive + */ +interface KeyDirective { + keyString: string; + resolvable: boolean; +} + /** * Visitor that converts GraphQL SDL to Protocol Buffer text definition * @@ -592,8 +600,11 @@ export class GraphQLToProtoTextVisitor { const normalizedKeysSet = new Set(); for (const keyDirective of keyDirectives) { - const keyString = this.getKeyFromDirective(keyDirective); - if (!keyString) continue; + const keyInfo = this.getKeyInfoFromDirective(keyDirective); + if (!keyInfo) continue; + + const { keyString, resolvable } = keyInfo; + if (!resolvable) continue; const normalizedKey = keyString .split(/[,\s]+/) @@ -1039,21 +1050,37 @@ Example: } /** - * Extract key fields from a directive + * Extract key info from a directive * * The @key directive specifies which fields form the entity's primary key. * We extract these for creating appropriate lookup methods. * * @param directive - The @key directive from the GraphQL AST - * @returns Array of field names that form the key + * @returns An object with the key fields and whether it is resolvable */ - private getKeyFromDirective(directive: DirectiveNode): string | null { - const fieldsArg = directive.arguments?.find((arg: ArgumentNode) => arg.name.value === 'fields'); - if (fieldsArg && fieldsArg.value.kind === 'StringValue') { - const stringValue = fieldsArg.value as StringValueNode; - return stringValue.value; + private getKeyInfoFromDirective(directive: DirectiveNode): KeyDirective | null { + const fieldsArgs = directive.arguments?.find((arg: ArgumentNode) => arg.name.value === 'fields'); + const resolvableArg = directive.arguments?.find((arg: ArgumentNode) => arg.name.value === 'resolvable'); + + if (!fieldsArgs && !resolvableArg) { + return null; + } + + const result: KeyDirective = { + keyString: '', + resolvable: true, + }; + + if (fieldsArgs && fieldsArgs.value.kind === 'StringValue') { + const stringValue = fieldsArgs.value as StringValueNode; + result.keyString = stringValue.value; } - return null; + + if (resolvableArg && resolvableArg.value.kind === 'BooleanValue') { + result.resolvable = resolvableArg.value.value; + } + + return result; } /** diff --git a/protographic/tests/sdl-to-proto/04-federation.test.ts b/protographic/tests/sdl-to-proto/04-federation.test.ts index 2ec9f0d585..fc9a1d9d88 100644 --- a/protographic/tests/sdl-to-proto/04-federation.test.ts +++ b/protographic/tests/sdl-to-proto/04-federation.test.ts @@ -842,6 +842,232 @@ describe('SDL to Proto - Federation and Special Types', () => { repeated Product products = 1; } + message Product { + string id = 1; + string manufacturer_id = 2; + string product_code = 3; + string name = 4; + double price = 5; + }" + `); + }); + test('should not generate lookup methods for non-resolvable keys', () => { + const sdl = ` + scalar openfed__FieldSet + directive @key(fields: openfed__FieldSet!, resolvable: Boolean = true) repeatable on INTERFACE | OBJECT + + type Product @key(fields: "id", resolvable: false) @key(fields: "manufacturerId productCode", resolvable: false) { + id: ID! + manufacturerId: ID! + productCode: String! + name: String! + price: Float! + } + + type Query { + products: [Product!]! + } + `; + + const { proto: protoText } = compileGraphQLToProto(sdl); + + // Validate Proto definition + expectValidProto(protoText); + + // Check that no lookup methods are generated for non-resolvable keys + expect(protoText).toMatchInlineSnapshot(` + "syntax = "proto3"; + package service.v1; + + // Service definition for DefaultService + service DefaultService { + rpc QueryProducts(QueryProductsRequest) returns (QueryProductsResponse) {} + } + + // Request message for products operation. + message QueryProductsRequest { + } + // Response message for products operation. + message QueryProductsResponse { + repeated Product products = 1; + } + + message Product { + string id = 1; + string manufacturer_id = 2; + string product_code = 3; + string name = 4; + double price = 5; + }" + `); + }); + test('should generate lookup methods for resolvable keys', () => { + const sdl = ` + scalar openfed__FieldSet + directive @key(fields: openfed__FieldSet!, resolvable: Boolean = true) repeatable on INTERFACE | OBJECT + + type Product @key(fields: "id", resolvable: false) @key(fields: "manufacturerId productCode", resolvable: true) { + id: ID! + manufacturerId: ID! + productCode: String! + name: String! + price: Float! + } + + type Query { + products: [Product!]! + } + `; + + const { proto: protoText } = compileGraphQLToProto(sdl); + + // Validate Proto definition + expectValidProto(protoText); + + // Expect that only the resolvable key is generated + expect(protoText).toMatchInlineSnapshot(` + "syntax = "proto3"; + package service.v1; + + // Service definition for DefaultService + service DefaultService { + // Lookup Product entity by manufacturerId and productCode + rpc LookupProductByManufacturerIdAndProductCode(LookupProductByManufacturerIdAndProductCodeRequest) returns (LookupProductByManufacturerIdAndProductCodeResponse) {} + rpc QueryProducts(QueryProductsRequest) returns (QueryProductsResponse) {} + } + + // Key message for Product entity lookup + message LookupProductByManufacturerIdAndProductCodeRequestKey { + // Key field for Product entity lookup. + string manufacturer_id = 1; + // Key field for Product entity lookup. + string product_code = 2; + } + + // Request message for Product entity lookup. + message LookupProductByManufacturerIdAndProductCodeRequest { + /* + * List of keys to look up Product entities. + * Order matters - each key maps to one entity in LookupProductByManufacturerIdAndProductCodeResponse. + */ + repeated LookupProductByManufacturerIdAndProductCodeRequestKey keys = 1; + } + + // Response message for Product entity lookup. + message LookupProductByManufacturerIdAndProductCodeResponse { + /* + * List of Product entities in the same order as the keys in LookupProductByManufacturerIdAndProductCodeRequest. + * Always return the same number of entities as keys. Use null for entities that cannot be found. + * + * Example: + * LookupUserByIdRequest: + * keys: + * - id: 1 + * - id: 2 + * LookupUserByIdResponse: + * result: + * - id: 1 # User with id 1 found + * - null # User with id 2 not found + */ + repeated Product result = 1; + } + + // Request message for products operation. + message QueryProductsRequest { + } + // Response message for products operation. + message QueryProductsResponse { + repeated Product products = 1; + } + + message Product { + string id = 1; + string manufacturer_id = 2; + string product_code = 3; + string name = 4; + double price = 5; + }" + `); + }); + test('should generate lookup method when resolvable is not specified', () => { + const sdl = ` + scalar openfed__FieldSet + directive @key(fields: openfed__FieldSet!, resolvable: Boolean = true) repeatable on INTERFACE | OBJECT + + type Product @key(fields: "id", resolvable: false) @key(fields: "manufacturerId productCode") { + id: ID! + manufacturerId: ID! + productCode: String! + name: String! + price: Float! + } + + type Query { + products: [Product!]! + } + `; + + const { proto: protoText } = compileGraphQLToProto(sdl); + + // Validate Proto definition + expectValidProto(protoText); + + // Expect that only the resolvable key is generated + expect(protoText).toMatchInlineSnapshot(` + "syntax = "proto3"; + package service.v1; + + // Service definition for DefaultService + service DefaultService { + // Lookup Product entity by manufacturerId and productCode + rpc LookupProductByManufacturerIdAndProductCode(LookupProductByManufacturerIdAndProductCodeRequest) returns (LookupProductByManufacturerIdAndProductCodeResponse) {} + rpc QueryProducts(QueryProductsRequest) returns (QueryProductsResponse) {} + } + + // Key message for Product entity lookup + message LookupProductByManufacturerIdAndProductCodeRequestKey { + // Key field for Product entity lookup. + string manufacturer_id = 1; + // Key field for Product entity lookup. + string product_code = 2; + } + + // Request message for Product entity lookup. + message LookupProductByManufacturerIdAndProductCodeRequest { + /* + * List of keys to look up Product entities. + * Order matters - each key maps to one entity in LookupProductByManufacturerIdAndProductCodeResponse. + */ + repeated LookupProductByManufacturerIdAndProductCodeRequestKey keys = 1; + } + + // Response message for Product entity lookup. + message LookupProductByManufacturerIdAndProductCodeResponse { + /* + * List of Product entities in the same order as the keys in LookupProductByManufacturerIdAndProductCodeRequest. + * Always return the same number of entities as keys. Use null for entities that cannot be found. + * + * Example: + * LookupUserByIdRequest: + * keys: + * - id: 1 + * - id: 2 + * LookupUserByIdResponse: + * result: + * - id: 1 # User with id 1 found + * - null # User with id 2 not found + */ + repeated Product result = 1; + } + + // Request message for products operation. + message QueryProductsRequest { + } + // Response message for products operation. + message QueryProductsResponse { + repeated Product products = 1; + } + message Product { string id = 1; string manufacturer_id = 2;