Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 37 additions & 10 deletions protographic/src/sdl-to-proto-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -592,8 +600,11 @@ export class GraphQLToProtoTextVisitor {

const normalizedKeysSet = new Set<string>();
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]+/)
Expand Down Expand Up @@ -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;
}
Comment thread
Noroth marked this conversation as resolved.

/**
Expand Down
226 changes: 226 additions & 0 deletions protographic/tests/sdl-to-proto/04-federation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down