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
23 changes: 8 additions & 15 deletions protographic/src/naming-conventions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,22 +50,15 @@ export function createResponseMessageName(methodName: string): string {
/**
* Creates an entity lookup method name for an entity type
*/
export function createEntityLookupMethodName(typeName: string, keyField: string = 'id'): string {
return `Lookup${typeName}By${upperFirst(camelCase(keyField))}`;
}
export function createEntityLookupMethodName(typeName: string, keyString: string = 'id'): string {
const normalizedKey = keyString
.split(/[,\s]+/)
.filter((field) => field.length > 0)
.map((field) => upperFirst(camelCase(field)))
.sort()
.join('And');

/**
* Creates a request message name for an entity lookup
*/
export function createEntityLookupRequestName(typeName: string, keyField: string = 'id'): string {
return `Lookup${typeName}By${upperFirst(camelCase(keyField))}Request`;
}

/**
* Creates a response message name for an entity lookup
*/
export function createEntityLookupResponseName(typeName: string, keyField: string = 'id'): string {
return `Lookup${typeName}By${upperFirst(camelCase(keyField))}Response`;
return `Lookup${typeName}By${normalizedKey}`;
}

/**
Expand Down
47 changes: 25 additions & 22 deletions protographic/src/sdl-to-mapping-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import {
} from 'graphql';
import {
createEntityLookupMethodName,
createEntityLookupRequestName,
createEntityLookupResponseName,
createOperationMethodName,
createRequestMessageName,
createResponseMessageName,
Expand Down Expand Up @@ -116,26 +114,29 @@ export class GraphQLToProtoVisitor {

// Check if this is an entity type (has @key directive)
if (isObjectType(type)) {
const keyDirective = this.getKeyDirective(type);
if (!keyDirective) continue;

const keyFields = this.getKeyFieldsFromDirective(keyDirective);
if (keyFields.length > 0) {
// Create entity mapping using the first key field
this.createEntityMapping(typeName, keyFields[0]);
const keyDirectives = this.getKeyDirectives(type);
if (keyDirectives.length === 0) continue;

// Process each @key directive separately
for (const keyDirective of keyDirectives) {
const key = this.getKeyFromDirective(keyDirective);
if (key) {
// Create entity mapping for each key combination
this.createEntityMapping(typeName, key);
}
}
}
}
}

/**
* Extract the key directive from a GraphQL object type
* Extract all key directives from a GraphQL object type
*
* @param type - The GraphQL object type to check for key directive
* @returns The key directive if found, undefined otherwise
* @param type - The GraphQL object type to check for key directives
* @returns Array of all key directives found
*/
private getKeyDirective(type: GraphQLObjectType): DirectiveNode | undefined {
return type.astNode?.directives?.find((d) => d.name.value === 'key');
private getKeyDirectives(type: GraphQLObjectType): DirectiveNode[] {
return type.astNode?.directives?.filter((d) => d.name.value === 'key') || [];
}

/**
Expand All @@ -148,13 +149,15 @@ export class GraphQLToProtoVisitor {
* @param keyField - The field that serves as the entity's key
*/
private createEntityMapping(typeName: string, keyField: string): void {
const rpc = createEntityLookupMethodName(typeName, keyField);

const entityMapping = new EntityMapping({
typeName,
kind: 'entity',
key: keyField,
rpc: createEntityLookupMethodName(typeName, keyField),
request: createEntityLookupRequestName(typeName, keyField),
response: createEntityLookupResponseName(typeName, keyField),
rpc: rpc,
request: createRequestMessageName(rpc),
response: createResponseMessageName(rpc),
});

this.mapping.entityMappings.push(entityMapping);
Expand All @@ -164,18 +167,18 @@ export class GraphQLToProtoVisitor {
* Extract key fields from a @key directive
*
* The @key directive specifies which fields form the entity's primary key
* in Federation. This method extracts those field names.
* in Federation. This method extracts the key string as-is.
*
* @param directive - The @key directive from the GraphQL AST
* @returns Array of field names that form the key
* @returns The key string (e.g. "id" or "id upc")
*/
private getKeyFieldsFromDirective(directive: DirectiveNode): string[] {
private getKeyFromDirective(directive: DirectiveNode): string | null {
// Extract fields argument from the key directive
const fieldsArg = directive.arguments?.find((arg) => arg.name.value === 'fields');
if (fieldsArg && fieldsArg.value.kind === Kind.STRING) {
return fieldsArg.value.value.split(' ');
return fieldsArg.value.value;
}
return [];
return null;
}

/**
Expand Down
91 changes: 62 additions & 29 deletions protographic/src/sdl-to-proto-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ import {
} from 'graphql';
import {
createEntityLookupMethodName,
createEntityLookupRequestName,
createEntityLookupResponseName,
createEnumUnspecifiedValue,
createOperationMethodName,
createRequestMessageName,
Expand Down Expand Up @@ -551,30 +549,47 @@ export class GraphQLToProtoTextVisitor {

// Check if this is an entity type (has @key directive)
if (isObjectType(type)) {
const astNode = type.astNode;
const keyDirective = astNode?.directives?.find((d) => d.name.value === 'key');
const keyDirectives = this.getKeyDirectives(type);

if (keyDirective) {
// Queue this type for message generation
if (keyDirectives.length > 0) {
// Queue this type for message generation (only once)
this.queueTypeForProcessing(type);

const keyFields = this.getKeyFieldsFromDirective(keyDirective);
if (keyFields.length > 0) {
const keyField = keyFields[0];
const methodName = createEntityLookupMethodName(typeName, keyField);
const requestName = createEntityLookupRequestName(typeName, keyField);
const responseName = createEntityLookupResponseName(typeName, keyField);
// Normalize keys by sorting fields alphabetically and deduplicating

const normalizedKeysSet = new Set<string>();
for (const keyDirective of keyDirectives) {
const keyString = this.getKeyFromDirective(keyDirective);
if (!keyString) continue;

const normalizedKey = keyString
.split(/[,\s]+/)
.filter((field) => field.length > 0)
.sort()
.join(' ');

normalizedKeysSet.add(normalizedKey);
}

// Process each normalized key
for (const normalizedKeyString of normalizedKeysSet) {
const methodName = createEntityLookupMethodName(typeName, normalizedKeyString);

const requestName = createRequestMessageName(methodName);
const responseName = createResponseMessageName(methodName);

// Add method name and RPC method with description from the entity type
result.methodNames.push(methodName);
const description = `Lookup ${typeName} entity by ${keyField}${
const keyFields = normalizedKeyString.split(' ');
const keyDescription = keyFields.length === 1 ? keyFields[0] : keyFields.join(' and ');
const description = `Lookup ${typeName} entity by ${keyDescription}${
type.description ? ': ' + type.description : ''
}`;
result.rpcMethods.push(this.createRpcMethod(methodName, requestName, responseName, description));

// Create request and response messages
// Create request and response messages for this key combination
result.messageDefinitions.push(
...this.createKeyRequestMessage(typeName, requestName, keyFields[0], responseName),
...this.createKeyRequestMessage(typeName, requestName, normalizedKeyString, responseName),
);
result.messageDefinitions.push(...this.createKeyResponseMessage(typeName, responseName, requestName));
}
Expand Down Expand Up @@ -697,7 +712,7 @@ export class GraphQLToProtoTextVisitor {
private createKeyRequestMessage(
typeName: string,
requestName: string,
keyField: string,
keyString: string,
responseName: string,
): string[] {
const messageLines: string[] = [];
Expand All @@ -717,28 +732,36 @@ export class GraphQLToProtoTextVisitor {
messageLines.push(` reserved ${this.formatReservedNumbers(keyMessageLock.reservedNumbers)};`);
}

const keyFields = keyString.split(' ');

// Check for field removals in the key message
if (lockData.messages[keyMessageName]) {
const originalKeyFieldNames = Object.keys(lockData.messages[keyMessageName].fields);
const currentKeyFieldNames = [graphqlFieldToProtoField(keyField)];
const currentKeyFieldNames = keyFields.map((field) => graphqlFieldToProtoField(field));
this.trackRemovedFields(keyMessageName, originalKeyFieldNames, currentKeyFieldNames);
}

const protoKeyField = graphqlFieldToProtoField(keyField);
// Add all key fields to the key message
const protoKeyFields: string[] = [];
keyFields.forEach((keyField, index) => {
const protoKeyField = graphqlFieldToProtoField(keyField);
protoKeyFields.push(protoKeyField);

// Get the appropriate field number for the key field
const keyFieldNumber = this.getFieldNumber(keyMessageName, protoKeyField, 1);
// Get the appropriate field number for this key field
const keyFieldNumber = this.getFieldNumber(keyMessageName, protoKeyField, index + 1);

if (this.includeComments) {
const keyFieldComment = `Key field for ${typeName} entity lookup.`;
messageLines.push(...this.formatComment(keyFieldComment, 1)); // Field comment, indent 1 level
}
messageLines.push(` string ${protoKeyField} = ${keyFieldNumber};`);
});

if (this.includeComments) {
const keyFieldComment = `Key field for ${typeName} entity lookup.`;
messageLines.push(...this.formatComment(keyFieldComment, 1)); // Field comment, indent 1 level
}
messageLines.push(` string ${protoKeyField} = ${keyFieldNumber};`);
messageLines.push('}');
messageLines.push('');

// Ensure the key message is registered in the lock manager data
this.lockManager.reconcileMessageFieldOrder(keyMessageName, [protoKeyField]);
this.lockManager.reconcileMessageFieldOrder(keyMessageName, protoKeyFields);

// Now create the main request message with a repeated key field
// Check for field removals in the request message
Expand Down Expand Up @@ -972,6 +995,16 @@ Example:
return messageLines;
}

/**
* Extract all key directives from a GraphQL object type
*
* @param type - The GraphQL object type to check for key directives
* @returns Array of all key directives found
*/
private getKeyDirectives(type: GraphQLObjectType): DirectiveNode[] {
return type.astNode?.directives?.filter((d) => d.name.value === 'key') || [];
}

/**
* Extract key fields from a directive
*
Expand All @@ -981,13 +1014,13 @@ Example:
* @param directive - The @key directive from the GraphQL AST
* @returns Array of field names that form the key
*/
private getKeyFieldsFromDirective(directive: DirectiveNode): string[] {
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.split(' ');
return stringValue.value;
}
return [];
return null;
}

/**
Expand Down
Loading