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
811 changes: 408 additions & 403 deletions demo/pkg/subgraphs/projects/generated/service.pb.go

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion demo/pkg/subgraphs/projects/generated/service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,8 @@ message Task {
google.protobuf.StringValue description = 6;
TaskPriority priority = 7;
TaskStatus status = 8;
google.protobuf.DoubleValue estimated_hours = 9;
// Deprecation notice: No more estimations!
google.protobuf.DoubleValue estimated_hours = 9 [deprecated = true];
google.protobuf.DoubleValue actual_hours = 10;
google.protobuf.StringValue created_at = 11;
google.protobuf.StringValue completed_at = 12;
Expand Down
2 changes: 1 addition & 1 deletion demo/pkg/subgraphs/projects/src/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ type Task implements Node & Assignable @key(fields: "id") {
description: String
priority: TaskPriority!
status: TaskStatus!
estimatedHours: Float
estimatedHours: Float @deprecated(reason: "No more estimations!")
actualHours: Float
createdAt: String # ISO date
completedAt: String # ISO date
Expand Down
137 changes: 131 additions & 6 deletions protographic/src/sdl-to-proto-visitor.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import {
ArgumentNode,
ConstValueNode,
DirectiveNode,
getNamedType,
GraphQLEnumType,
GraphQLEnumValue,
GraphQLField,
GraphQLInputField,
GraphQLInputObjectType,
GraphQLInterfaceType,
GraphQLList,
Expand All @@ -21,6 +24,7 @@ import {
isObjectType,
isScalarType,
isUnionType,
Kind,
StringValueNode,
} from 'graphql';
import {
Expand Down Expand Up @@ -459,7 +463,7 @@ export class GraphQLToProtoTextVisitor {
}

// Build the complete proto file
const protoContent: string[] = [];
let protoContent: string[] = [];

// Add the header (syntax, package, imports, options)
protoContent.push(...this.buildProtoHeader());
Expand Down Expand Up @@ -513,6 +517,13 @@ export class GraphQLToProtoTextVisitor {
protoContent.push(messageDef);
}

protoContent = this.trimEmptyLines(protoContent);
this.protoText = this.trimEmptyLines(this.protoText);

if (this.protoText.length > 0) {
protoContent.push('');
}

// Add all processed types from protoText (populated by processMessageQueue)
protoContent.push(...this.protoText);

Expand All @@ -522,6 +533,28 @@ export class GraphQLToProtoTextVisitor {
return protoContent.join('\n');
}

/**
* Trim empty lines from the beginning and end of the array
*/
private trimEmptyLines(data: string[]): string[] {
// Find the first non-empty line index
const firstNonEmpty = data.findIndex((line) => line.trim() !== '');

// If no non-empty lines found, return empty array
if (firstNonEmpty === -1) {
return [];
}

// Find the last non-empty line index by searching backwards
let lastNonEmpty = data.length - 1;
while (lastNonEmpty >= 0 && data[lastNonEmpty].trim() === '') {
lastNonEmpty--;
}

// Return slice from first to last non-empty line (inclusive)
return data.slice(firstNonEmpty, lastNonEmpty + 1);
}

/**
* Collects RPC methods for entity types (types with @key directive)
*
Expand Down Expand Up @@ -1146,6 +1179,7 @@ Example:
const field = fields[fieldName];
const fieldType = this.getProtoTypeFromGraphQL(field.type);
const protoFieldName = graphqlFieldToProtoField(fieldName);
const deprecationInfo = this.fieldIsDeprecated(field, [...type.getInterfaces()]);

// Get the appropriate field number, respecting the lock
const fieldNumber = this.getFieldNumber(type.name, protoFieldName, this.getNextAvailableFieldNumber(type.name));
Expand All @@ -1155,10 +1189,21 @@ Example:
this.protoText.push(...this.formatComment(field.description, 1)); // Field comment, indent 1 level
}

if (deprecationInfo.deprecated && deprecationInfo.reason && deprecationInfo.reason.length > 0) {
this.protoText.push(...this.formatComment(`Deprecation notice: ${deprecationInfo.reason}`, 1));
}

const fieldOptions = [];
if (deprecationInfo.deprecated) {
fieldOptions.push(` [deprecated = true]`);
}

if (fieldType.isRepeated) {
this.protoText.push(` repeated ${fieldType.typeName} ${protoFieldName} = ${fieldNumber};`);
this.protoText.push(
` repeated ${fieldType.typeName} ${protoFieldName} = ${fieldNumber}${fieldOptions.join(' ')};`,
);
} else {
this.protoText.push(` ${fieldType.typeName} ${protoFieldName} = ${fieldNumber};`);
this.protoText.push(` ${fieldType.typeName} ${protoFieldName} = ${fieldNumber}${fieldOptions.join(' ')};`);
}

// Queue complex field types for processing
Expand All @@ -1172,6 +1217,63 @@ Example:
this.protoText.push('}');
}

/**
* Resolve deprecation for a field (optionally considering interface fields)
* Field-level reason takes precedence; otherwise the first interface with a non-empty reason wins.
* @param field - The GraphQL field to handle directives for
* @param interfaces - The GraphQL interfaces that the field implements
* @returns An object with the deprecated flag and the reason for deprecation
*/
private fieldIsDeprecated(
field: GraphQLField<any, any> | GraphQLInputField,
interfaces: GraphQLInterfaceType[],
): { deprecated: boolean; reason?: string } {
const allFieldsRefs = [
field,
...interfaces.map((iface) => iface.getFields()[field.name]).filter((f) => f !== undefined),
];

const deprecatedDirectives = allFieldsRefs
.map((f) => f.astNode?.directives?.find((d) => d.name.value === 'deprecated'))
.filter((d) => d !== undefined);

if (deprecatedDirectives.length === 0) {
return { deprecated: false };
}

const reasons = deprecatedDirectives
.map((d) => d.arguments?.find((a) => a.name.value === 'reason')?.value)
.filter((r) => r !== undefined && this.isNonEmptyStringValueNode(r));

if (reasons.length === 0) {
return { deprecated: true };
}

return { deprecated: true, reason: reasons[0]?.value };
}

private enumValueIsDeprecated(value: GraphQLEnumValue): { deprecated: boolean; reason?: string } {
const deprecatedDirective = value.astNode?.directives?.find((d) => d.name.value === 'deprecated');
if (!deprecatedDirective) {
return { deprecated: false };
}
const reasonNode = deprecatedDirective.arguments?.find((a) => a.name.value === 'reason')?.value;
if (this.isNonEmptyStringValueNode(reasonNode)) {
return { deprecated: true, reason: reasonNode.value.trim() };
}

return { deprecated: true };
}

/**
* Check if a node is a non-empty string value node
* @param node - The node to check
* @returns True if the node is a non-empty string value node, false otherwise
*/
private isNonEmptyStringValueNode(node: ConstValueNode | undefined): node is StringValueNode {
return node?.kind === Kind.STRING && node.value.trim().length > 0;
}

/**
* Process a GraphQL input object type to a Proto message
*
Expand Down Expand Up @@ -1217,6 +1319,7 @@ Example:
const field = fields[fieldName];
const fieldType = this.getProtoTypeFromGraphQL(field.type);
const protoFieldName = graphqlFieldToProtoField(fieldName);
const deprecationInfo = this.fieldIsDeprecated(field, []);

// Get the appropriate field number, respecting the lock
const fieldNumber = this.getFieldNumber(type.name, protoFieldName, this.getNextAvailableFieldNumber(type.name));
Expand All @@ -1226,10 +1329,21 @@ Example:
this.protoText.push(...this.formatComment(field.description, 1)); // Field comment, indent 1 level
}

if (deprecationInfo.deprecated && deprecationInfo.reason && deprecationInfo.reason.length > 0) {
this.protoText.push(...this.formatComment(`Deprecation notice: ${deprecationInfo.reason}`, 1));
}

const fieldOptions = [];
if (deprecationInfo.deprecated) {
fieldOptions.push(` [deprecated = true]`);
}

if (fieldType.isRepeated) {
this.protoText.push(` repeated ${fieldType.typeName} ${protoFieldName} = ${fieldNumber};`);
this.protoText.push(
` repeated ${fieldType.typeName} ${protoFieldName} = ${fieldNumber}${fieldOptions.join(' ')};`,
);
} else {
this.protoText.push(` ${fieldType.typeName} ${protoFieldName} = ${fieldNumber};`);
this.protoText.push(` ${fieldType.typeName} ${protoFieldName} = ${fieldNumber}${fieldOptions.join(' ')};`);
}

// Queue complex field types for processing
Expand Down Expand Up @@ -1416,11 +1530,17 @@ Example:

const protoEnumValue = graphqlEnumValueToProtoEnumValue(type.name, value.name);

const deprecationInfo = this.enumValueIsDeprecated(value);

// Add enum value description as comment
if (value.description) {
this.protoText.push(...this.formatComment(value.description, 1)); // Field comment, indent 1 level
}

if (deprecationInfo.deprecated && (deprecationInfo.reason?.length ?? 0) > 0) {
this.protoText.push(...this.formatComment(`Deprecation notice: ${deprecationInfo.reason}`, 1));
}

// Get value number from lock data
const lockData = this.lockManager.getLockData();
let valueNumber = 0;
Expand All @@ -1433,7 +1553,12 @@ Example:
continue;
}

this.protoText.push(` ${protoEnumValue} = ${valueNumber};`);
const fieldOptions = [];
if (deprecationInfo.deprecated) {
fieldOptions.push(` [deprecated = true]`);
}

this.protoText.push(` ${protoEnumValue} = ${valueNumber}${fieldOptions.join(' ')};`);
}

this.indent--;
Expand Down
Loading