@wundergraph/protographic 0.3.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ Protographic bridges GraphQL and Protocol Buffers (protobuf) ecosystems through
|
|
|
18
18
|
- Robust handling of complex GraphQL features (unions, interfaces, directives)
|
|
19
19
|
- First-class support for Federation entity mapping
|
|
20
20
|
- Deterministic field ordering with proto.lock.json for backward compatibility
|
|
21
|
+
- Use of Protocol Buffer wrappers for nullable fields to distinguish between semantic nulls and zero values
|
|
21
22
|
|
|
22
23
|
## Installation
|
|
23
24
|
|
|
@@ -31,10 +31,16 @@ export declare class GraphQLToProtoTextVisitor {
|
|
|
31
31
|
private readonly schema;
|
|
32
32
|
/** The name of the Protocol Buffer service */
|
|
33
33
|
private readonly serviceName;
|
|
34
|
+
/** The package name for the proto file */
|
|
35
|
+
private readonly packageName;
|
|
34
36
|
/** The lock manager for deterministic ordering */
|
|
35
37
|
private readonly lockManager;
|
|
36
38
|
/** Generated proto lock data */
|
|
37
39
|
private generatedLockData;
|
|
40
|
+
/** List of import statements */
|
|
41
|
+
private imports;
|
|
42
|
+
/** List of option statements */
|
|
43
|
+
private options;
|
|
38
44
|
/** Accumulates the Protocol Buffer definition text */
|
|
39
45
|
private protoText;
|
|
40
46
|
/** Current indentation level for formatted output */
|
|
@@ -47,6 +53,8 @@ export declare class GraphQLToProtoTextVisitor {
|
|
|
47
53
|
private messageQueue;
|
|
48
54
|
/** Track generated nested list wrapper messages */
|
|
49
55
|
private nestedListWrappers;
|
|
56
|
+
/** Track whether wrapper types are used (for conditional import) */
|
|
57
|
+
private usesWrapperTypes;
|
|
50
58
|
/**
|
|
51
59
|
* Map of message names to their field numbers for tracking deleted fields
|
|
52
60
|
* This maintains field numbers even when fields are removed from the schema
|
|
@@ -93,6 +101,18 @@ export declare class GraphQLToProtoTextVisitor {
|
|
|
93
101
|
* removed from the schema but may be re-added in the future.
|
|
94
102
|
*/
|
|
95
103
|
private getNextAvailableFieldNumber;
|
|
104
|
+
/**
|
|
105
|
+
* Add an import statement to the proto file
|
|
106
|
+
*/
|
|
107
|
+
private addImport;
|
|
108
|
+
/**
|
|
109
|
+
* Add an option statement to the proto file
|
|
110
|
+
*/
|
|
111
|
+
private addOption;
|
|
112
|
+
/**
|
|
113
|
+
* Build the proto file header with syntax, package, imports, and options
|
|
114
|
+
*/
|
|
115
|
+
private buildProtoHeader;
|
|
96
116
|
/**
|
|
97
117
|
* Visit the GraphQL schema to generate Proto buffer definition
|
|
98
118
|
*
|
|
@@ -234,16 +254,72 @@ export declare class GraphQLToProtoTextVisitor {
|
|
|
234
254
|
* Map GraphQL type to Protocol Buffer type
|
|
235
255
|
*
|
|
236
256
|
* Determines the appropriate Protocol Buffer type for a given GraphQL type,
|
|
237
|
-
*
|
|
257
|
+
* including the use of wrapper types for nullable scalar fields to distinguish
|
|
258
|
+
* between unset fields and zero values.
|
|
238
259
|
*
|
|
239
260
|
* @param graphqlType - The GraphQL type to convert
|
|
261
|
+
* @param ignoreWrapperTypes - If true, do not use wrapper types for nullable scalar fields
|
|
240
262
|
* @returns The corresponding Protocol Buffer type name
|
|
241
263
|
*/
|
|
242
264
|
private getProtoTypeFromGraphQL;
|
|
243
265
|
/**
|
|
244
|
-
*
|
|
266
|
+
* Converts GraphQL list types to appropriate Protocol Buffer representations.
|
|
267
|
+
*
|
|
268
|
+
* For non-nullable, single-level lists (e.g., [String!]!), generates simple repeated fields.
|
|
269
|
+
* For nullable lists (e.g., [String]) or nested lists (e.g., [[String]]), creates wrapper
|
|
270
|
+
* messages to properly handle nullability in proto3.
|
|
271
|
+
*
|
|
272
|
+
* Examples:
|
|
273
|
+
* - [String!]! → repeated string field_name = 1;
|
|
274
|
+
* - [String] → ListOfString field_name = 1; (with wrapper message)
|
|
275
|
+
* - [[String!]!]! → ListOfListOfString field_name = 1; (with nested wrapper messages)
|
|
276
|
+
* - [[String]] → ListOfListOfString field_name = 1; (with nested wrapper messages)
|
|
277
|
+
*
|
|
278
|
+
* @param graphqlType - The GraphQL list type to convert
|
|
279
|
+
* @returns ProtoType object containing the type name and whether it should be repeated
|
|
280
|
+
*/
|
|
281
|
+
private handleListType;
|
|
282
|
+
/**
|
|
283
|
+
* Unwraps a GraphQL type from a GraphQLNonNull type
|
|
284
|
+
*/
|
|
285
|
+
private unwrapNonNullType;
|
|
286
|
+
/**
|
|
287
|
+
* Checks if a GraphQL list type contains nested lists
|
|
288
|
+
* Type guard that narrows the input type when nested lists are detected
|
|
289
|
+
*/
|
|
290
|
+
private isNestedListType;
|
|
291
|
+
/**
|
|
292
|
+
* Calculates the nesting level of a GraphQL list type
|
|
293
|
+
*/
|
|
294
|
+
private calculateNestingLevel;
|
|
295
|
+
/**
|
|
296
|
+
* Creates wrapper messages for nullable or nested GraphQL lists.
|
|
297
|
+
*
|
|
298
|
+
* Generates Protocol Buffer message definitions to handle list nullability and nesting.
|
|
299
|
+
* The wrapper messages are stored and later included in the final proto output.
|
|
300
|
+
*
|
|
301
|
+
* For level 1: Creates simple wrapper like:
|
|
302
|
+
* message ListOfString {
|
|
303
|
+
* repeated string items = 1;
|
|
304
|
+
* }
|
|
305
|
+
*
|
|
306
|
+
* For level > 1: Creates nested wrapper structures like:
|
|
307
|
+
* message ListOfListOfString {
|
|
308
|
+
* message List {
|
|
309
|
+
* repeated ListOfString items = 1;
|
|
310
|
+
* }
|
|
311
|
+
* List list = 1;
|
|
312
|
+
* }
|
|
313
|
+
*
|
|
314
|
+
* @param level - The nesting level (1 for simple wrapper, >1 for nested structures)
|
|
315
|
+
* @param baseType - The GraphQL base type being wrapped (e.g., String, User, etc.)
|
|
316
|
+
* @returns The generated wrapper message name (e.g., "ListOfString", "ListOfListOfUser")
|
|
245
317
|
*/
|
|
246
318
|
private createNestedListWrapper;
|
|
319
|
+
/**
|
|
320
|
+
* Builds the message lines for a wrapper message
|
|
321
|
+
*/
|
|
322
|
+
private buildWrapperMessage;
|
|
247
323
|
/**
|
|
248
324
|
* Get indentation based on the current level
|
|
249
325
|
*
|
|
@@ -15,6 +15,19 @@ const SCALAR_TYPE_MAP = {
|
|
|
15
15
|
Float: 'double', // Using double for GraphQL Float gives better precision
|
|
16
16
|
Boolean: 'bool', // Direct mapping
|
|
17
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* Maps GraphQL scalar types to Protocol Buffer wrapper types for nullable fields
|
|
20
|
+
*
|
|
21
|
+
* These wrapper types allow distinguishing between unset fields and zero values
|
|
22
|
+
* in Protocol Buffers, which is important for GraphQL nullable semantics.
|
|
23
|
+
*/
|
|
24
|
+
const SCALAR_WRAPPER_TYPE_MAP = {
|
|
25
|
+
ID: 'google.protobuf.StringValue',
|
|
26
|
+
String: 'google.protobuf.StringValue',
|
|
27
|
+
Int: 'google.protobuf.Int32Value',
|
|
28
|
+
Float: 'google.protobuf.DoubleValue',
|
|
29
|
+
Boolean: 'google.protobuf.BoolValue',
|
|
30
|
+
};
|
|
18
31
|
/**
|
|
19
32
|
* Visitor that converts GraphQL SDL to Protocol Buffer text definition
|
|
20
33
|
*
|
|
@@ -40,6 +53,10 @@ export class GraphQLToProtoTextVisitor {
|
|
|
40
53
|
constructor(schema, options = {}) {
|
|
41
54
|
/** Generated proto lock data */
|
|
42
55
|
this.generatedLockData = null;
|
|
56
|
+
/** List of import statements */
|
|
57
|
+
this.imports = [];
|
|
58
|
+
/** List of option statements */
|
|
59
|
+
this.options = [];
|
|
43
60
|
/** Accumulates the Protocol Buffer definition text */
|
|
44
61
|
this.protoText = [];
|
|
45
62
|
/** Current indentation level for formatted output */
|
|
@@ -50,6 +67,8 @@ export class GraphQLToProtoTextVisitor {
|
|
|
50
67
|
this.messageQueue = [];
|
|
51
68
|
/** Track generated nested list wrapper messages */
|
|
52
69
|
this.nestedListWrappers = new Map();
|
|
70
|
+
/** Track whether wrapper types are used (for conditional import) */
|
|
71
|
+
this.usesWrapperTypes = false;
|
|
53
72
|
/**
|
|
54
73
|
* Map of message names to their field numbers for tracking deleted fields
|
|
55
74
|
* This maintains field numbers even when fields are removed from the schema
|
|
@@ -58,21 +77,20 @@ export class GraphQLToProtoTextVisitor {
|
|
|
58
77
|
const { serviceName = 'DefaultService', packageName = 'service.v1', goPackage, lockData, includeComments = true, } = options;
|
|
59
78
|
this.schema = schema;
|
|
60
79
|
this.serviceName = serviceName;
|
|
80
|
+
this.packageName = packageName;
|
|
61
81
|
this.lockManager = new ProtoLockManager(lockData);
|
|
62
82
|
this.includeComments = includeComments;
|
|
63
83
|
// If we have lock data, initialize the field numbers map
|
|
64
84
|
if (lockData) {
|
|
65
85
|
this.initializeFieldNumbersMap(lockData);
|
|
66
86
|
}
|
|
67
|
-
|
|
87
|
+
// Initialize options
|
|
68
88
|
if (goPackage && goPackage !== '') {
|
|
69
89
|
// Generate default go_package if not provided
|
|
70
90
|
const defaultGoPackage = `cosmo/pkg/proto/${packageName};${packageName.replace('.', '')}`;
|
|
71
91
|
const goPackageOption = goPackage || defaultGoPackage;
|
|
72
|
-
|
|
92
|
+
this.options.push(`option go_package = "${goPackageOption}";`);
|
|
73
93
|
}
|
|
74
|
-
// Initialize the Proto definition with the standard header
|
|
75
|
-
this.protoText = ['syntax = "proto3";', `package ${packageName};`, '', ...protoOptions];
|
|
76
94
|
}
|
|
77
95
|
/**
|
|
78
96
|
* Initialize the field numbers map from the lock data to preserve field numbers
|
|
@@ -229,15 +247,58 @@ export class GraphQLToProtoTextVisitor {
|
|
|
229
247
|
// Find the maximum field number and add 1
|
|
230
248
|
return Math.max(...usedNumbers) + 1;
|
|
231
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* Add an import statement to the proto file
|
|
252
|
+
*/
|
|
253
|
+
addImport(importPath) {
|
|
254
|
+
if (!this.imports.includes(importPath)) {
|
|
255
|
+
this.imports.push(importPath);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Add an option statement to the proto file
|
|
260
|
+
*/
|
|
261
|
+
addOption(optionStatement) {
|
|
262
|
+
if (!this.options.includes(optionStatement)) {
|
|
263
|
+
this.options.push(optionStatement);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Build the proto file header with syntax, package, imports, and options
|
|
268
|
+
*/
|
|
269
|
+
buildProtoHeader() {
|
|
270
|
+
const header = [];
|
|
271
|
+
// Add syntax declaration
|
|
272
|
+
header.push('syntax = "proto3";');
|
|
273
|
+
// Add package declaration
|
|
274
|
+
header.push(`package ${this.packageName};`);
|
|
275
|
+
header.push('');
|
|
276
|
+
// Add options if any (options come before imports)
|
|
277
|
+
if (this.options.length > 0) {
|
|
278
|
+
// Sort options for consistent output
|
|
279
|
+
const sortedOptions = [...this.options].sort();
|
|
280
|
+
for (const option of sortedOptions) {
|
|
281
|
+
header.push(option);
|
|
282
|
+
}
|
|
283
|
+
header.push('');
|
|
284
|
+
}
|
|
285
|
+
// Add imports if any
|
|
286
|
+
if (this.imports.length > 0) {
|
|
287
|
+
// Sort imports for consistent output
|
|
288
|
+
const sortedImports = [...this.imports].sort();
|
|
289
|
+
for (const importPath of sortedImports) {
|
|
290
|
+
header.push(`import "${importPath}";`);
|
|
291
|
+
}
|
|
292
|
+
header.push('');
|
|
293
|
+
}
|
|
294
|
+
return header;
|
|
295
|
+
}
|
|
232
296
|
/**
|
|
233
297
|
* Visit the GraphQL schema to generate Proto buffer definition
|
|
234
298
|
*
|
|
235
299
|
* @returns The complete Protocol Buffer definition as a string
|
|
236
300
|
*/
|
|
237
301
|
visit() {
|
|
238
|
-
// Clear the protoText array to just contain the header
|
|
239
|
-
const headerText = this.protoText.slice();
|
|
240
|
-
this.protoText = [];
|
|
241
302
|
// Collect RPC methods and message definitions from all sources
|
|
242
303
|
const entityResult = this.collectEntityRpcMethods();
|
|
243
304
|
const queryResult = this.collectQueryRpcMethods();
|
|
@@ -252,15 +313,23 @@ export class GraphQLToProtoTextVisitor {
|
|
|
252
313
|
];
|
|
253
314
|
// Add all types from the schema to the queue that weren't already queued
|
|
254
315
|
this.queueAllSchemaTypes();
|
|
255
|
-
//
|
|
256
|
-
this.
|
|
316
|
+
// Process all complex types from the message queue to determine if wrapper types are needed
|
|
317
|
+
this.processMessageQueue();
|
|
318
|
+
// Add wrapper import if needed
|
|
319
|
+
if (this.usesWrapperTypes) {
|
|
320
|
+
this.addImport('google/protobuf/wrappers.proto');
|
|
321
|
+
}
|
|
322
|
+
// Build the complete proto file
|
|
323
|
+
const protoContent = [];
|
|
324
|
+
// Add the header (syntax, package, imports, options)
|
|
325
|
+
protoContent.push(...this.buildProtoHeader());
|
|
257
326
|
// Add a service description comment
|
|
258
327
|
if (this.includeComments) {
|
|
259
328
|
const serviceComment = `Service definition for ${this.serviceName}`;
|
|
260
|
-
|
|
329
|
+
protoContent.push(...this.formatComment(serviceComment, 0)); // Top-level comment, no indent
|
|
261
330
|
}
|
|
262
|
-
//
|
|
263
|
-
|
|
331
|
+
// Add service block containing RPC methods
|
|
332
|
+
protoContent.push(`service ${this.serviceName} {`);
|
|
264
333
|
this.indent++;
|
|
265
334
|
// Sort method names deterministically by alphabetical order
|
|
266
335
|
const orderedMethodNames = [...allMethodNames].sort();
|
|
@@ -273,35 +342,35 @@ export class GraphQLToProtoTextVisitor {
|
|
|
273
342
|
if (rpcMethodText.includes('\n')) {
|
|
274
343
|
// For multi-line RPC method definitions (with comments), add each line separately
|
|
275
344
|
const lines = rpcMethodText.split('\n');
|
|
276
|
-
|
|
345
|
+
protoContent.push(...lines);
|
|
277
346
|
}
|
|
278
347
|
else {
|
|
279
348
|
// For simple one-line RPC method definitions (ensure 2-space indentation)
|
|
280
|
-
|
|
349
|
+
protoContent.push(` ${rpcMethodText}`);
|
|
281
350
|
}
|
|
282
351
|
}
|
|
283
352
|
}
|
|
284
353
|
// Close service definition
|
|
285
354
|
this.indent--;
|
|
286
|
-
|
|
287
|
-
|
|
355
|
+
protoContent.push('}');
|
|
356
|
+
protoContent.push('');
|
|
288
357
|
// Add all wrapper messages first since they might be referenced by other messages
|
|
289
358
|
if (this.nestedListWrappers.size > 0) {
|
|
290
359
|
// Sort the wrappers by name for deterministic output
|
|
291
360
|
const sortedWrapperNames = Array.from(this.nestedListWrappers.keys()).sort();
|
|
292
361
|
for (const wrapperName of sortedWrapperNames) {
|
|
293
|
-
|
|
362
|
+
protoContent.push(this.nestedListWrappers.get(wrapperName));
|
|
294
363
|
}
|
|
295
364
|
}
|
|
296
|
-
//
|
|
365
|
+
// Add all message definitions
|
|
297
366
|
for (const messageDef of allMessageDefinitions) {
|
|
298
|
-
|
|
367
|
+
protoContent.push(messageDef);
|
|
299
368
|
}
|
|
300
|
-
//
|
|
301
|
-
this.
|
|
369
|
+
// Add all processed types from protoText (populated by processMessageQueue)
|
|
370
|
+
protoContent.push(...this.protoText);
|
|
302
371
|
// Store the generated lock data for retrieval
|
|
303
372
|
this.generatedLockData = this.lockManager.getLockData();
|
|
304
|
-
return
|
|
373
|
+
return protoContent.join('\n');
|
|
305
374
|
}
|
|
306
375
|
/**
|
|
307
376
|
* Collects RPC methods for entity types (types with @key directive)
|
|
@@ -597,12 +666,11 @@ Example:
|
|
|
597
666
|
messageLines.push(...this.formatComment(arg.description, 1));
|
|
598
667
|
}
|
|
599
668
|
// Check if the argument is a list type and add the repeated keyword if needed
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
messageLines.push(` repeated ${argType} ${argProtoName} = ${fieldNumber};`);
|
|
669
|
+
if (argType.isRepeated) {
|
|
670
|
+
messageLines.push(` repeated ${argType.typeName} ${argProtoName} = ${fieldNumber};`);
|
|
603
671
|
}
|
|
604
672
|
else {
|
|
605
|
-
messageLines.push(` ${argType} ${argProtoName} = ${fieldNumber};`);
|
|
673
|
+
messageLines.push(` ${argType.typeName} ${argProtoName} = ${fieldNumber};`);
|
|
606
674
|
}
|
|
607
675
|
// Add complex input types to the queue for processing
|
|
608
676
|
const namedType = getNamedType(arg.type);
|
|
@@ -645,7 +713,6 @@ Example:
|
|
|
645
713
|
messageLines.push(` reserved ${this.formatReservedNumbers(messageLock.reservedNumbers)};`);
|
|
646
714
|
}
|
|
647
715
|
const returnType = this.getProtoTypeFromGraphQL(field.type);
|
|
648
|
-
const isRepeated = isListType(field.type) || (isNonNullType(field.type) && isListType(field.type.ofType));
|
|
649
716
|
// Get the appropriate field number, respecting the lock
|
|
650
717
|
const fieldNumber = this.getFieldNumber(responseName, protoFieldName, 1);
|
|
651
718
|
// Add description for the response field based on field description
|
|
@@ -653,11 +720,11 @@ Example:
|
|
|
653
720
|
// Use 1 level indent for field comments
|
|
654
721
|
messageLines.push(...this.formatComment(field.description, 1));
|
|
655
722
|
}
|
|
656
|
-
if (isRepeated) {
|
|
657
|
-
messageLines.push(` repeated ${returnType} ${protoFieldName} = ${fieldNumber};`);
|
|
723
|
+
if (returnType.isRepeated) {
|
|
724
|
+
messageLines.push(` repeated ${returnType.typeName} ${protoFieldName} = ${fieldNumber};`);
|
|
658
725
|
}
|
|
659
726
|
else {
|
|
660
|
-
messageLines.push(` ${returnType} ${protoFieldName} = ${fieldNumber};`);
|
|
727
|
+
messageLines.push(` ${returnType.typeName} ${protoFieldName} = ${fieldNumber};`);
|
|
661
728
|
}
|
|
662
729
|
messageLines.push('}');
|
|
663
730
|
// Ensure this message is registered in the lock manager data
|
|
@@ -788,7 +855,6 @@ Example:
|
|
|
788
855
|
continue;
|
|
789
856
|
const field = fields[fieldName];
|
|
790
857
|
const fieldType = this.getProtoTypeFromGraphQL(field.type);
|
|
791
|
-
const isRepeated = isListType(field.type) || (isNonNullType(field.type) && isListType(field.type.ofType));
|
|
792
858
|
const protoFieldName = graphqlFieldToProtoField(fieldName);
|
|
793
859
|
// Get the appropriate field number, respecting the lock
|
|
794
860
|
const fieldNumber = this.getFieldNumber(type.name, protoFieldName, this.getNextAvailableFieldNumber(type.name));
|
|
@@ -796,11 +862,11 @@ Example:
|
|
|
796
862
|
if (field.description) {
|
|
797
863
|
this.protoText.push(...this.formatComment(field.description, 1)); // Field comment, indent 1 level
|
|
798
864
|
}
|
|
799
|
-
if (isRepeated) {
|
|
800
|
-
this.protoText.push(` repeated ${fieldType} ${protoFieldName} = ${fieldNumber};`);
|
|
865
|
+
if (fieldType.isRepeated) {
|
|
866
|
+
this.protoText.push(` repeated ${fieldType.typeName} ${protoFieldName} = ${fieldNumber};`);
|
|
801
867
|
}
|
|
802
868
|
else {
|
|
803
|
-
this.protoText.push(` ${fieldType} ${protoFieldName} = ${fieldNumber};`);
|
|
869
|
+
this.protoText.push(` ${fieldType.typeName} ${protoFieldName} = ${fieldNumber};`);
|
|
804
870
|
}
|
|
805
871
|
// Queue complex field types for processing
|
|
806
872
|
const namedType = getNamedType(field.type);
|
|
@@ -848,7 +914,6 @@ Example:
|
|
|
848
914
|
continue;
|
|
849
915
|
const field = fields[fieldName];
|
|
850
916
|
const fieldType = this.getProtoTypeFromGraphQL(field.type);
|
|
851
|
-
const isRepeated = isListType(field.type) || (isNonNullType(field.type) && isListType(field.type.ofType));
|
|
852
917
|
const protoFieldName = graphqlFieldToProtoField(fieldName);
|
|
853
918
|
// Get the appropriate field number, respecting the lock
|
|
854
919
|
const fieldNumber = this.getFieldNumber(type.name, protoFieldName, this.getNextAvailableFieldNumber(type.name));
|
|
@@ -856,11 +921,11 @@ Example:
|
|
|
856
921
|
if (field.description) {
|
|
857
922
|
this.protoText.push(...this.formatComment(field.description, 1)); // Field comment, indent 1 level
|
|
858
923
|
}
|
|
859
|
-
if (isRepeated) {
|
|
860
|
-
this.protoText.push(` repeated ${fieldType} ${protoFieldName} = ${fieldNumber};`);
|
|
924
|
+
if (fieldType.isRepeated) {
|
|
925
|
+
this.protoText.push(` repeated ${fieldType.typeName} ${protoFieldName} = ${fieldNumber};`);
|
|
861
926
|
}
|
|
862
927
|
else {
|
|
863
|
-
this.protoText.push(` ${fieldType} ${protoFieldName} = ${fieldNumber};`);
|
|
928
|
+
this.protoText.push(` ${fieldType.typeName} ${protoFieldName} = ${fieldNumber};`);
|
|
864
929
|
}
|
|
865
930
|
// Queue complex field types for processing
|
|
866
931
|
const namedType = getNamedType(field.type);
|
|
@@ -1036,90 +1101,178 @@ Example:
|
|
|
1036
1101
|
* Map GraphQL type to Protocol Buffer type
|
|
1037
1102
|
*
|
|
1038
1103
|
* Determines the appropriate Protocol Buffer type for a given GraphQL type,
|
|
1039
|
-
*
|
|
1104
|
+
* including the use of wrapper types for nullable scalar fields to distinguish
|
|
1105
|
+
* between unset fields and zero values.
|
|
1040
1106
|
*
|
|
1041
1107
|
* @param graphqlType - The GraphQL type to convert
|
|
1108
|
+
* @param ignoreWrapperTypes - If true, do not use wrapper types for nullable scalar fields
|
|
1042
1109
|
* @returns The corresponding Protocol Buffer type name
|
|
1043
1110
|
*/
|
|
1044
|
-
getProtoTypeFromGraphQL(graphqlType) {
|
|
1111
|
+
getProtoTypeFromGraphQL(graphqlType, ignoreWrapperTypes = false) {
|
|
1112
|
+
// Nullable lists need to be handled first, otherwise they will be treated as scalar types
|
|
1113
|
+
if (isListType(graphqlType) || (isNonNullType(graphqlType) && isListType(graphqlType.ofType))) {
|
|
1114
|
+
return this.handleListType(graphqlType);
|
|
1115
|
+
}
|
|
1116
|
+
// For nullable scalar types, use wrapper types
|
|
1045
1117
|
if (isScalarType(graphqlType)) {
|
|
1046
|
-
|
|
1118
|
+
if (ignoreWrapperTypes) {
|
|
1119
|
+
return { typeName: SCALAR_TYPE_MAP[graphqlType.name] || 'string', isRepeated: false };
|
|
1120
|
+
}
|
|
1121
|
+
this.usesWrapperTypes = true; // Track that we're using wrapper types
|
|
1122
|
+
return {
|
|
1123
|
+
typeName: SCALAR_WRAPPER_TYPE_MAP[graphqlType.name] || 'google.protobuf.StringValue',
|
|
1124
|
+
isRepeated: false,
|
|
1125
|
+
};
|
|
1047
1126
|
}
|
|
1048
1127
|
if (isEnumType(graphqlType)) {
|
|
1049
|
-
return graphqlType.name;
|
|
1128
|
+
return { typeName: graphqlType.name, isRepeated: false };
|
|
1050
1129
|
}
|
|
1051
1130
|
if (isNonNullType(graphqlType)) {
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
// Handle nested list types (e.g., [[Type]])
|
|
1056
|
-
const innerType = graphqlType.ofType;
|
|
1057
|
-
// If the inner type is also a list, we need to use a wrapper message
|
|
1058
|
-
if (isListType(innerType) || (isNonNullType(innerType) && isListType(innerType.ofType))) {
|
|
1059
|
-
// Find the most inner type by unwrapping all lists and non-nulls
|
|
1060
|
-
let currentType = innerType;
|
|
1061
|
-
while (isListType(currentType) || isNonNullType(currentType)) {
|
|
1062
|
-
currentType = isListType(currentType) ? currentType.ofType : currentType.ofType;
|
|
1063
|
-
}
|
|
1064
|
-
// Get the name of the inner type and create wrapper name
|
|
1065
|
-
const namedInnerType = currentType;
|
|
1066
|
-
const wrapperName = `${namedInnerType.name}List`;
|
|
1067
|
-
// Generate the wrapper message if not already created
|
|
1068
|
-
if (!this.processedTypes.has(wrapperName) && !this.nestedListWrappers.has(wrapperName)) {
|
|
1069
|
-
this.createNestedListWrapper(wrapperName, namedInnerType);
|
|
1070
|
-
}
|
|
1071
|
-
return wrapperName;
|
|
1131
|
+
// For non-null scalar types, use the base type
|
|
1132
|
+
if (isScalarType(graphqlType.ofType)) {
|
|
1133
|
+
return { typeName: SCALAR_TYPE_MAP[graphqlType.ofType.name] || 'string', isRepeated: false };
|
|
1072
1134
|
}
|
|
1073
|
-
return this.getProtoTypeFromGraphQL(
|
|
1135
|
+
return this.getProtoTypeFromGraphQL(graphqlType.ofType);
|
|
1074
1136
|
}
|
|
1075
1137
|
// Named types (object, interface, union, input)
|
|
1076
1138
|
const namedType = graphqlType;
|
|
1077
1139
|
if (namedType && typeof namedType.name === 'string') {
|
|
1078
|
-
return namedType.name;
|
|
1140
|
+
return { typeName: namedType.name, isRepeated: false };
|
|
1079
1141
|
}
|
|
1080
|
-
return 'string'; // Default fallback
|
|
1142
|
+
return { typeName: 'string', isRepeated: false }; // Default fallback
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Converts GraphQL list types to appropriate Protocol Buffer representations.
|
|
1146
|
+
*
|
|
1147
|
+
* For non-nullable, single-level lists (e.g., [String!]!), generates simple repeated fields.
|
|
1148
|
+
* For nullable lists (e.g., [String]) or nested lists (e.g., [[String]]), creates wrapper
|
|
1149
|
+
* messages to properly handle nullability in proto3.
|
|
1150
|
+
*
|
|
1151
|
+
* Examples:
|
|
1152
|
+
* - [String!]! → repeated string field_name = 1;
|
|
1153
|
+
* - [String] → ListOfString field_name = 1; (with wrapper message)
|
|
1154
|
+
* - [[String!]!]! → ListOfListOfString field_name = 1; (with nested wrapper messages)
|
|
1155
|
+
* - [[String]] → ListOfListOfString field_name = 1; (with nested wrapper messages)
|
|
1156
|
+
*
|
|
1157
|
+
* @param graphqlType - The GraphQL list type to convert
|
|
1158
|
+
* @returns ProtoType object containing the type name and whether it should be repeated
|
|
1159
|
+
*/
|
|
1160
|
+
handleListType(graphqlType) {
|
|
1161
|
+
const listType = this.unwrapNonNullType(graphqlType);
|
|
1162
|
+
const isNullableList = !isNonNullType(graphqlType);
|
|
1163
|
+
const isNestedList = this.isNestedListType(listType);
|
|
1164
|
+
// Simple non-nullable lists can use repeated fields directly
|
|
1165
|
+
if (!isNullableList && !isNestedList) {
|
|
1166
|
+
return { ...this.getProtoTypeFromGraphQL(getNamedType(listType), true), isRepeated: true };
|
|
1167
|
+
}
|
|
1168
|
+
// Nullable or nested lists need wrapper messages
|
|
1169
|
+
const baseType = getNamedType(listType);
|
|
1170
|
+
const nestingLevel = this.calculateNestingLevel(listType);
|
|
1171
|
+
// For nested lists, always use full nesting level to preserve inner list nullability
|
|
1172
|
+
// For single-level nullable lists, use nesting level 1
|
|
1173
|
+
const wrapperNestingLevel = isNestedList ? nestingLevel : 1;
|
|
1174
|
+
// Generate all required wrapper messages
|
|
1175
|
+
let wrapperName = '';
|
|
1176
|
+
for (let i = 1; i <= wrapperNestingLevel; i++) {
|
|
1177
|
+
wrapperName = this.createNestedListWrapper(i, baseType);
|
|
1178
|
+
}
|
|
1179
|
+
// For nested lists, never use repeated at field level to preserve nullability
|
|
1180
|
+
return { typeName: wrapperName, isRepeated: false };
|
|
1081
1181
|
}
|
|
1082
1182
|
/**
|
|
1083
|
-
*
|
|
1183
|
+
* Unwraps a GraphQL type from a GraphQLNonNull type
|
|
1084
1184
|
*/
|
|
1085
|
-
|
|
1086
|
-
|
|
1185
|
+
unwrapNonNullType(graphqlType) {
|
|
1186
|
+
return isNonNullType(graphqlType) ? graphqlType.ofType : graphqlType;
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* Checks if a GraphQL list type contains nested lists
|
|
1190
|
+
* Type guard that narrows the input type when nested lists are detected
|
|
1191
|
+
*/
|
|
1192
|
+
isNestedListType(listType) {
|
|
1193
|
+
return isListType(listType.ofType) || (isNonNullType(listType.ofType) && isListType(listType.ofType.ofType));
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Calculates the nesting level of a GraphQL list type
|
|
1197
|
+
*/
|
|
1198
|
+
calculateNestingLevel(listType) {
|
|
1199
|
+
let level = 1;
|
|
1200
|
+
let currentType = listType.ofType;
|
|
1201
|
+
while (true) {
|
|
1202
|
+
if (isNonNullType(currentType)) {
|
|
1203
|
+
currentType = currentType.ofType;
|
|
1204
|
+
}
|
|
1205
|
+
else if (isListType(currentType)) {
|
|
1206
|
+
currentType = currentType.ofType;
|
|
1207
|
+
level++;
|
|
1208
|
+
}
|
|
1209
|
+
else {
|
|
1210
|
+
break;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
return level;
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Creates wrapper messages for nullable or nested GraphQL lists.
|
|
1217
|
+
*
|
|
1218
|
+
* Generates Protocol Buffer message definitions to handle list nullability and nesting.
|
|
1219
|
+
* The wrapper messages are stored and later included in the final proto output.
|
|
1220
|
+
*
|
|
1221
|
+
* For level 1: Creates simple wrapper like:
|
|
1222
|
+
* message ListOfString {
|
|
1223
|
+
* repeated string items = 1;
|
|
1224
|
+
* }
|
|
1225
|
+
*
|
|
1226
|
+
* For level > 1: Creates nested wrapper structures like:
|
|
1227
|
+
* message ListOfListOfString {
|
|
1228
|
+
* message List {
|
|
1229
|
+
* repeated ListOfString items = 1;
|
|
1230
|
+
* }
|
|
1231
|
+
* List list = 1;
|
|
1232
|
+
* }
|
|
1233
|
+
*
|
|
1234
|
+
* @param level - The nesting level (1 for simple wrapper, >1 for nested structures)
|
|
1235
|
+
* @param baseType - The GraphQL base type being wrapped (e.g., String, User, etc.)
|
|
1236
|
+
* @returns The generated wrapper message name (e.g., "ListOfString", "ListOfListOfUser")
|
|
1237
|
+
*/
|
|
1238
|
+
createNestedListWrapper(level, baseType) {
|
|
1239
|
+
const wrapperName = `${'ListOf'.repeat(level)}${baseType.name}`;
|
|
1240
|
+
// Return existing wrapper if already created
|
|
1087
1241
|
if (this.processedTypes.has(wrapperName) || this.nestedListWrappers.has(wrapperName)) {
|
|
1088
|
-
return;
|
|
1242
|
+
return wrapperName;
|
|
1089
1243
|
}
|
|
1090
|
-
// Mark as processed to avoid recursion
|
|
1091
1244
|
this.processedTypes.add(wrapperName);
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
const
|
|
1101
|
-
// Add
|
|
1245
|
+
const messageLines = this.buildWrapperMessage(wrapperName, level, baseType);
|
|
1246
|
+
this.nestedListWrappers.set(wrapperName, messageLines.join('\n'));
|
|
1247
|
+
return wrapperName;
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Builds the message lines for a wrapper message
|
|
1251
|
+
*/
|
|
1252
|
+
buildWrapperMessage(wrapperName, level, baseType) {
|
|
1253
|
+
const lines = [];
|
|
1254
|
+
// Add comment if enabled
|
|
1102
1255
|
if (this.includeComments) {
|
|
1103
|
-
|
|
1104
|
-
|
|
1256
|
+
lines.push(...this.formatComment(`Wrapper message for a list of ${baseType.name}.`, 0));
|
|
1257
|
+
}
|
|
1258
|
+
lines.push(`message ${wrapperName} {`);
|
|
1259
|
+
const formatIndent = (indent, content) => {
|
|
1260
|
+
return ' '.repeat(indent) + content;
|
|
1261
|
+
};
|
|
1262
|
+
if (level > 1) {
|
|
1263
|
+
// Nested structure for deep lists
|
|
1264
|
+
const innerWrapperName = `${'ListOf'.repeat(level - 1)}${baseType.name}`;
|
|
1265
|
+
lines.push(formatIndent(1, `message List {`), formatIndent(2, `repeated ${innerWrapperName} items = 1;`), formatIndent(1, `}`));
|
|
1266
|
+
// Wrapper types always use deterministic field numbers - 'list' field is always 1
|
|
1267
|
+
lines.push(formatIndent(1, `List list = 1;`));
|
|
1105
1268
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
messageLines.push(` reserved ${this.formatReservedNumbers(messageLock.reservedNumbers)};`);
|
|
1269
|
+
else {
|
|
1270
|
+
// Simple repeated field for level 1 - 'items' field is always 1
|
|
1271
|
+
const protoType = this.getProtoTypeFromGraphQL(baseType, true);
|
|
1272
|
+
lines.push(formatIndent(1, `repeated ${protoType.typeName} items = 1;`));
|
|
1111
1273
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
// For the inner type, we need to get the proto type for the base type
|
|
1115
|
-
const protoType = this.getProtoTypeFromGraphQL(baseType);
|
|
1116
|
-
messageLines.push(` repeated ${protoType} result = ${fieldNumber};`);
|
|
1117
|
-
messageLines.push('}');
|
|
1118
|
-
messageLines.push('');
|
|
1119
|
-
// Ensure the wrapper message is registered in the lock manager data
|
|
1120
|
-
this.lockManager.reconcileMessageFieldOrder(wrapperName, ['result']);
|
|
1121
|
-
// Store the wrapper message for later inclusion in the output
|
|
1122
|
-
this.nestedListWrappers.set(wrapperName, messageLines.join('\n'));
|
|
1274
|
+
lines.push('}', '');
|
|
1275
|
+
return lines;
|
|
1123
1276
|
}
|
|
1124
1277
|
/**
|
|
1125
1278
|
* Get indentation based on the current level
|