Skip to content
1,171 changes: 643 additions & 528 deletions connect-go/gen/proto/wg/cosmo/node/v1/node.pb.go

Large diffs are not rendered by default.

71 changes: 71 additions & 0 deletions connect/src/wg/cosmo/node/v1/node_pb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2095,6 +2095,13 @@ export class EntityMapping extends Message<EntityMapping> {
*/
response = "";

/**
* Mappings for required fields
*
* @generated from field: repeated wg.cosmo.node.v1.RequiredFieldMapping required_field_mappings = 7;
*/
requiredFieldMappings: RequiredFieldMapping[] = [];

constructor(data?: PartialMessage<EntityMapping>) {
super();
proto3.util.initPartial(data, this);
Expand All @@ -2109,6 +2116,7 @@ export class EntityMapping extends Message<EntityMapping> {
{ no: 4, name: "rpc", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 5, name: "request", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 6, name: "response", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 7, name: "required_field_mappings", kind: "message", T: RequiredFieldMapping, repeated: true },
]);

static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): EntityMapping {
Expand All @@ -2128,6 +2136,69 @@ export class EntityMapping extends Message<EntityMapping> {
}
}

/**
* Defines mapping for required fields
*
* @generated from message wg.cosmo.node.v1.RequiredFieldMapping
*/
export class RequiredFieldMapping extends Message<RequiredFieldMapping> {
/**
* @generated from field: wg.cosmo.node.v1.FieldMapping field_mapping = 1;
*/
fieldMapping?: FieldMapping;

/**
* Mapped gRPC method name
*
* @generated from field: string rpc = 2;
*/
rpc = "";

/**
* gRPC request message type name
*
* @generated from field: string request = 3;
*/
request = "";

/**
* gRPC response message type name
*
* @generated from field: string response = 4;
*/
response = "";

constructor(data?: PartialMessage<RequiredFieldMapping>) {
super();
proto3.util.initPartial(data, this);
}

static readonly runtime: typeof proto3 = proto3;
static readonly typeName = "wg.cosmo.node.v1.RequiredFieldMapping";
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: "field_mapping", kind: "message", T: FieldMapping },
{ no: 2, name: "rpc", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 3, name: "request", kind: "scalar", T: 9 /* ScalarType.STRING */ },
{ no: 4, name: "response", kind: "scalar", T: 9 /* ScalarType.STRING */ },
]);

static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): RequiredFieldMapping {
return new RequiredFieldMapping().fromBinary(bytes, options);
}

static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): RequiredFieldMapping {
return new RequiredFieldMapping().fromJson(jsonValue, options);
}

static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): RequiredFieldMapping {
return new RequiredFieldMapping().fromJsonString(jsonString, options);
}

static equals(a: RequiredFieldMapping | PlainMessage<RequiredFieldMapping> | undefined, b: RequiredFieldMapping | PlainMessage<RequiredFieldMapping> | undefined): boolean {
return proto3.util.equals(RequiredFieldMapping, a, b);
}
}

/**
* Defines mapping between GraphQL type fields and gRPC message fields
*
Expand Down
13 changes: 13 additions & 0 deletions proto/wg/cosmo/node/v1/node.proto
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,19 @@ message EntityMapping {
string request = 5;
// gRPC response message type name
string response = 6;
// Mappings for required fields
repeated RequiredFieldMapping required_field_mappings = 7;
}

// Defines mapping for required fields
message RequiredFieldMapping {
FieldMapping field_mapping = 1;
// Mapped gRPC method name
string rpc = 2;
// gRPC request message type name
string request = 3;
// gRPC response message type name
string response = 4;
}

// Defines mapping between GraphQL type fields and gRPC message fields
Expand Down
108 changes: 108 additions & 0 deletions protographic/src/abstract-selection-rewriter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {
ASTVisitor,
DocumentNode,
GraphQLSchema,
GraphQLObjectType,
visit,
SelectionSetNode,
isInterfaceType,
Kind,
FieldNode,
ASTNode,
GraphQLField,
GraphQLType,
getNamedType,
} from 'graphql';
import { VisitContext } from './types';

// TODO: The full functionality will be implemented in the second iteration.
/**
* AbstractSelectionRewriter is a visitor implementation that normalizes an operation document
* by rewriting abstract type selections for interfaces to the concrete types.
*
* This normalizes the operation and allows us to determine the proper types needed to generate proto messages.
*
*/
export class AbstractSelectionRewriter {
private readonly visitor: ASTVisitor;
private readonly fieldSetDoc: DocumentNode;
public readonly schema: GraphQLSchema;
private normalizedFiedSetDoc: DocumentNode | undefined;

private ancestors: GraphQLObjectType[] = [];
private currentType: GraphQLObjectType;

constructor(fieldSetDoc: DocumentNode, schema: GraphQLSchema, objectType: GraphQLObjectType) {
this.fieldSetDoc = fieldSetDoc;
this.schema = schema;
this.currentType = objectType;
this.visitor = this.createASTVisitor();
}

private createASTVisitor(): ASTVisitor {
return {
SelectionSet: {
enter: (node, key, parent, path, ancestors) => {
this.onEnterSelectionSet({ node, key, parent, path, ancestors });
},
},
};
}

public normalize(): void {
visit(this.fieldSetDoc, this.visitor);
}

private onEnterSelectionSet(ctx: VisitContext<SelectionSetNode>): void {
if (!ctx.parent) return;
if (!this.isFieldNode(ctx.parent)) return;

const currentType = this.findNamedTypeForField(ctx.parent.name.value);
if (!currentType) return;

if (!isInterfaceType(currentType)) {
return;
}

const fields = ctx.node.selections.filter((s) => s.kind === Kind.FIELD);
const inlineFragments = ctx.node.selections.filter((s) => s.kind === Kind.INLINE_FRAGMENT);

// remove the fields from the selection set.
ctx.node.selections = [...inlineFragments];

for (const fragment of inlineFragments) {
const normalizedFields = fragment.selectionSet.selections.filter((s) => s.kind === Kind.FIELD) ?? [];

for (const field of fields) {
if (this.hasField(normalizedFields, field.name.value)) {
continue;
}

normalizedFields.unshift(field);
}

fragment.selectionSet.selections = [...normalizedFields];
}
}

private hasField(fields: FieldNode[], fieldName: string): boolean {
return fields.some((f) => f.name.value === fieldName);
}

private isFieldNode(node: ASTNode | ReadonlyArray<ASTNode>): node is FieldNode {
if (Array.isArray(node)) return false;
return (node as ASTNode).kind === Kind.FIELD;
}

private fieldDefinition(fieldName: string): GraphQLField<any, any, any> | undefined {
return this.currentType.getFields()[fieldName];
}

private findNamedTypeForField(fieldName: string): GraphQLType | undefined {
const fields = this.currentType.getFields();
const field = fields[fieldName];
if (!field) return undefined;

return getNamedType(field.type);
}
}
1 change: 1 addition & 0 deletions protographic/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export function validateGraphQLSDL(sdl: string): ValidationResult {

export * from './sdl-to-mapping-visitor.js';
export { GraphQLToProtoTextVisitor } from './sdl-to-proto-visitor.js';
export { RequiredFieldsVisitor } from './required-fields-visitor.js';
export { ProtoLockManager } from './proto-lock.js';
export { SDLValidationVisitor } from './sdl-validation-visitor.js';

Expand Down
39 changes: 38 additions & 1 deletion protographic/src/naming-conventions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,51 @@ export function createResponseMessageName(methodName: string): string {
* Creates an entity lookup method name for an entity type
*/
export function createEntityLookupMethodName(typeName: string, keyString: string = 'id'): string {
const normalizedKey = createMethodSuffixFromEntityKey(keyString);
return `Lookup${typeName}${normalizedKey}`;
}

/**
* Creates a key message name for an entity lookup request
* @param typeName - The name of the entity type
* @param keyString - The key string
* @returns The name of the key message
*/
export function createEntityLookupRequestKeyMessageName(typeName: string, keyString: string = 'id'): string {
const requestName = createRequestMessageName(createEntityLookupMethodName(typeName, keyString));
return `${requestName}Key`;
}

/**
* Creates a required fields method name for an entity type
* @param typeName - The name of the entity type
* @param fieldName - The name of the field that is required
* @param keyString - The key string
* @returns The name of the required fields method
* @example
* createRequiredFieldsMethodName('User', 'post', 'id') // => 'RequireUserPostById'
* createRequiredFieldsMethodName('User', 'post', 'id name') // => 'RequireUserPostByIdAndName'
* createRequiredFieldsMethodName('User', 'post', 'name,id') // => 'RequireUserPostByNameAndId'
*/
export function createRequiredFieldsMethodName(typeName: string, fieldName: string, keyString: string = 'id'): string {
const normalizedKey = createMethodSuffixFromEntityKey(keyString);
return `Require${typeName}${upperFirst(camelCase(fieldName))}${normalizedKey}`;
}

/**
* Creates a method suffix from an entity key string
* @param keyString - The key string
* @returns The method suffix
*/
export function createMethodSuffixFromEntityKey(keyString: string = 'id'): string {
const normalizedKey = keyString
.split(/[,\s]+/)
.filter((field) => field.length > 0)
.map((field) => upperFirst(camelCase(field)))
.sort()
.join('And');

return `Lookup${typeName}By${normalizedKey}`;
return `By${normalizedKey}`;
}

/**
Expand Down
Loading
Loading