Skip to content

Commit 8368b3b

Browse files
committed
type safety
1 parent 35e5ca7 commit 8368b3b

File tree

3 files changed

+50
-30
lines changed

3 files changed

+50
-30
lines changed

protographic/src/operations/message-builder.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
isInterfaceType,
1919
isUnionType,
2020
GraphQLInterfaceType,
21+
GraphQLUnionType,
2122
} from 'graphql';
2223
import { mapGraphQLTypeToProto, ProtoTypeInfo } from './type-mapper.js';
2324
import { FieldNumberManager } from './field-numbering.js';
@@ -58,7 +59,7 @@ export interface MessageBuilderOptions {
5859
export function buildMessageFromSelectionSet(
5960
messageName: string,
6061
selectionSet: SelectionSetNode,
61-
parentType: GraphQLObjectType,
62+
parentType: GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType,
6263
typeInfo: TypeInfo,
6364
options?: MessageBuilderOptions,
6465
): protobuf.Type {
@@ -71,7 +72,7 @@ export function buildMessageFromSelectionSet(
7172

7273
const collectFields = (
7374
selections: readonly SelectionNode[],
74-
currentType: GraphQLObjectType | GraphQLInterfaceType,
75+
currentType: GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType,
7576
) => {
7677
for (const selection of selections) {
7778
if (selection.kind === 'Field') {
@@ -151,7 +152,7 @@ export function buildMessageFromSelectionSet(
151152
function processFieldSelection(
152153
field: FieldNode,
153154
message: protobuf.Type,
154-
parentType: GraphQLObjectType | GraphQLInterfaceType,
155+
parentType: GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType,
155156
typeInfo: TypeInfo,
156157
options?: MessageBuilderOptions,
157158
fieldNumberManager?: FieldNumberManager,
@@ -171,6 +172,13 @@ function processFieldSelection(
171172
}
172173

173174
// Get the field definition from the parent type
175+
// Union types don't have fields directly, so skip field validation for them
176+
if (isUnionType(parentType)) {
177+
// Union types should only be processed through inline fragments
178+
// This shouldn't happen in normal GraphQL, but we'll handle it gracefully
179+
return;
180+
}
181+
174182
const fieldDef = parentType.getFields()[fieldName];
175183
if (!fieldDef) {
176184
throw new Error(
@@ -188,9 +196,9 @@ function processFieldSelection(
188196
// Use simple name since message will be nested inside parent
189197
const nestedMessageName = upperFirst(camelCase(fieldName));
190198

191-
// For interfaces and unions, we use the base type to collect fields from inline fragments
192-
// For object types, we process normally
193-
const typeForSelection = isObjectType(namedType) ? namedType : (namedType as any);
199+
// For interfaces and unions, we use the type directly for processing
200+
// Union types will only work with inline fragments that specify concrete types
201+
const typeForSelection = namedType;
194202

195203
const nestedMessage = buildMessageFromSelectionSet(
196204
nestedMessageName,
@@ -300,13 +308,13 @@ function processFieldSelection(
300308
function processInlineFragment(
301309
fragment: InlineFragmentNode,
302310
message: protobuf.Type,
303-
parentType: GraphQLObjectType,
311+
parentType: GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType,
304312
typeInfo: TypeInfo,
305313
options?: MessageBuilderOptions,
306314
fieldNumberManager?: FieldNumberManager,
307315
): void {
308316
// Determine the type for this inline fragment
309-
let fragmentType: GraphQLObjectType;
317+
let fragmentType: GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType;
310318

311319
if (fragment.typeCondition) {
312320
// Type condition specified: ... on User
@@ -319,8 +327,8 @@ function processInlineFragment(
319327
}
320328

321329
const type = schema.getType(typeName);
322-
if (!type || !isObjectType(type)) {
323-
// Type not found or not an object type - skip
330+
if (!type || !(isObjectType(type) || isInterfaceType(type) || isUnionType(type))) {
331+
// Type not found or not a supported type - skip
324332
return;
325333
}
326334

@@ -352,7 +360,7 @@ function processInlineFragment(
352360
function processFragmentSpread(
353361
spread: FragmentSpreadNode,
354362
message: protobuf.Type,
355-
parentType: GraphQLObjectType,
363+
parentType: GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType,
356364
typeInfo: TypeInfo,
357365
options?: MessageBuilderOptions,
358366
fieldNumberManager?: FieldNumberManager,

protographic/tests/operations/request-builder.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, test } from 'vitest';
2-
import { buildSchema, parse } from 'graphql';
2+
import { buildSchema, parse, GraphQLInputObjectType, GraphQLEnumType } from 'graphql';
33
import { buildRequestMessage, buildInputObjectMessage, buildEnumType } from '../../src/operations/request-builder';
44
import { createFieldNumberManager } from '../../src/operations/field-numbering';
55

@@ -244,7 +244,7 @@ describe('Request Builder', () => {
244244
throw new Error('Invalid input type');
245245
}
246246

247-
const message = buildInputObjectMessage(inputType as any);
247+
const message = buildInputObjectMessage(inputType as GraphQLInputObjectType);
248248

249249
expect(message.name).toBe('UserInput');
250250
expect(message.fieldsArray).toHaveLength(3);
@@ -270,7 +270,7 @@ describe('Request Builder', () => {
270270
throw new Error('Invalid input type');
271271
}
272272

273-
const message = buildInputObjectMessage(inputType as any);
273+
const message = buildInputObjectMessage(inputType as GraphQLInputObjectType);
274274

275275
expect(message.name).toBe('UserInput');
276276
expect(message.fields.profile).toBeDefined();
@@ -292,7 +292,7 @@ describe('Request Builder', () => {
292292

293293
const manager = createFieldNumberManager();
294294

295-
const message = buildInputObjectMessage(inputType as any, {
295+
const message = buildInputObjectMessage(inputType as GraphQLInputObjectType, {
296296
fieldNumberManager: manager,
297297
});
298298

@@ -316,7 +316,7 @@ describe('Request Builder', () => {
316316
throw new Error('Invalid enum type');
317317
}
318318

319-
const protoEnum = buildEnumType(enumType as any);
319+
const protoEnum = buildEnumType(enumType as GraphQLEnumType);
320320

321321
expect(protoEnum.name).toBe('Status');
322322
expect(protoEnum.values.UNSPECIFIED).toBe(0);
@@ -338,7 +338,7 @@ describe('Request Builder', () => {
338338
throw new Error('Invalid enum type');
339339
}
340340

341-
const protoEnum = buildEnumType(enumType as any);
341+
const protoEnum = buildEnumType(enumType as GraphQLEnumType);
342342

343343
expect(protoEnum.values.UNSPECIFIED).toBe(0);
344344
expect(protoEnum.values.ADMIN).toBeGreaterThan(0);
@@ -359,7 +359,7 @@ describe('Request Builder', () => {
359359
throw new Error('Invalid enum type');
360360
}
361361

362-
const protoEnum = buildEnumType(enumType as any);
362+
const protoEnum = buildEnumType(enumType as GraphQLEnumType);
363363

364364
expect(protoEnum.values.UNSPECIFIED).toBe(0);
365365
expect(protoEnum.values.LOW).toBe(1);

protographic/tests/util.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -136,19 +136,31 @@ export function getReservedNumbers(root: protobufjs.Root, typeName: string, isEn
136136
return [];
137137
}
138138

139-
// Type assertion needed because protobufjs doesn't expose reserved property in type definitions
140-
const typeWithReserved = type as any;
141-
return (
142-
typeWithReserved.reserved?.map((range: number | { start: number; end: number }) => {
143-
if (typeof range === 'number') {
144-
return range;
145-
} else if (range.start === range.end) {
146-
return range.start;
139+
// Use the existing reserved property from protobufjs types
140+
if (!type.reserved) {
141+
return [];
142+
}
143+
144+
const numbers: number[] = [];
145+
for (const range of type.reserved) {
146+
if (typeof range === 'string') {
147+
// Skip string reserved fields (field names)
148+
continue;
149+
}
150+
if (Array.isArray(range)) {
151+
// Handle number arrays [start, end]
152+
if (range.length === 2) {
153+
const [start, end] = range;
154+
for (let i = start; i <= end; i++) {
155+
numbers.push(i);
156+
}
157+
} else {
158+
numbers.push(...range);
147159
}
148-
// For ranges, just return the start for simplicity
149-
return range.start;
150-
}) || []
151-
);
160+
}
161+
}
162+
163+
return numbers;
152164
}
153165

154166
/**

0 commit comments

Comments
 (0)