@wundergraph/protographic 0.15.5 → 0.16.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.
Files changed (51) hide show
  1. package/dist/src/abstract-selection-rewriter.d.ts +265 -0
  2. package/dist/src/abstract-selection-rewriter.js +585 -0
  3. package/dist/src/abstract-selection-rewriter.js.map +1 -0
  4. package/dist/src/index.d.ts +2 -0
  5. package/dist/src/index.js +4 -2
  6. package/dist/src/index.js.map +1 -1
  7. package/dist/src/naming-conventions.d.ts +59 -0
  8. package/dist/src/naming-conventions.js +82 -7
  9. package/dist/src/naming-conventions.js.map +1 -1
  10. package/dist/src/operation-to-proto.js +9 -6
  11. package/dist/src/operation-to-proto.js.map +1 -1
  12. package/dist/src/operations/field-numbering.js +6 -3
  13. package/dist/src/operations/field-numbering.js.map +1 -1
  14. package/dist/src/operations/message-builder.js +38 -23
  15. package/dist/src/operations/message-builder.js.map +1 -1
  16. package/dist/src/operations/proto-field-options.js.map +1 -1
  17. package/dist/src/operations/proto-text-generator.js +16 -27
  18. package/dist/src/operations/proto-text-generator.js.map +1 -1
  19. package/dist/src/operations/request-builder.d.ts +5 -0
  20. package/dist/src/operations/request-builder.js +14 -3
  21. package/dist/src/operations/request-builder.js.map +1 -1
  22. package/dist/src/operations/type-mapper.js +3 -22
  23. package/dist/src/operations/type-mapper.js.map +1 -1
  24. package/dist/src/proto-lock.js +19 -19
  25. package/dist/src/proto-lock.js.map +1 -1
  26. package/dist/src/proto-utils.d.ts +74 -0
  27. package/dist/src/proto-utils.js +286 -0
  28. package/dist/src/proto-utils.js.map +1 -0
  29. package/dist/src/required-fields-visitor.d.ts +230 -0
  30. package/dist/src/required-fields-visitor.js +513 -0
  31. package/dist/src/required-fields-visitor.js.map +1 -0
  32. package/dist/src/sdl-to-mapping-visitor.d.ts +9 -1
  33. package/dist/src/sdl-to-mapping-visitor.js +72 -12
  34. package/dist/src/sdl-to-mapping-visitor.js.map +1 -1
  35. package/dist/src/sdl-to-proto-visitor.d.ts +20 -66
  36. package/dist/src/sdl-to-proto-visitor.js +279 -398
  37. package/dist/src/sdl-to-proto-visitor.js.map +1 -1
  38. package/dist/src/sdl-validation-visitor.d.ts +2 -0
  39. package/dist/src/sdl-validation-visitor.js +85 -29
  40. package/dist/src/sdl-validation-visitor.js.map +1 -1
  41. package/dist/src/selection-set-validation-visitor.d.ts +112 -0
  42. package/dist/src/selection-set-validation-visitor.js +199 -0
  43. package/dist/src/selection-set-validation-visitor.js.map +1 -0
  44. package/dist/src/string-constants.d.ts +4 -0
  45. package/dist/src/string-constants.js +4 -0
  46. package/dist/src/string-constants.js.map +1 -1
  47. package/dist/src/types.d.ts +102 -0
  48. package/dist/src/types.js +37 -1
  49. package/dist/src/types.js.map +1 -1
  50. package/dist/tsconfig.tsbuildinfo +1 -1
  51. package/package.json +9 -5
@@ -1,36 +1,11 @@
1
- import { getNamedType, GraphQLID, isEnumType, isInputObjectType, isInterfaceType, isListType, isNamedType, isNonNullType, isObjectType, isScalarType, isUnionType, Kind, } from 'graphql';
2
- import { createEntityLookupMethodName, createEnumUnspecifiedValue, createOperationMethodName, createRequestMessageName, createResolverMethodName, createResponseMessageName, graphqlEnumValueToProtoEnumValue, graphqlFieldToProtoField, resolverResponseResultName, typeFieldArgsName, typeFieldContextName, } from './naming-conventions.js';
1
+ import { getNamedType, GraphQLID, isEnumType, isInputObjectType, isInterfaceType, isNamedType, isObjectType, isScalarType, isUnionType, Kind, } from 'graphql';
3
2
  import { camelCase } from 'lodash-es';
3
+ import { createEntityLookupMethodName, createEnumUnspecifiedValue, createOperationMethodName, createRequestMessageName, createResolverMethodName, createResponseMessageName, graphqlEnumValueToProtoEnumValue, graphqlFieldToProtoField, resolverResponseResultName, typeFieldArgsName, typeFieldContextName, } from './naming-conventions.js';
4
4
  import { ProtoLockManager } from './proto-lock.js';
5
- import { CONNECT_FIELD_RESOLVER, CONTEXT, FIELD_ARGS, RESULT } from './string-constants.js';
6
- import { unwrapNonNullType, isNestedListType, calculateNestingLevel } from './operations/list-type-utils.js';
5
+ import { CONNECT_FIELD_RESOLVER, CONTEXT, EXTERNAL_DIRECTIVE_NAME, FIELD_ARGS, KEY_DIRECTIVE_NAME, REQUIRES_DIRECTIVE_NAME, RESULT, } from './string-constants.js';
7
6
  import { buildProtoOptions } from './proto-options.js';
8
- /**
9
- * Maps GraphQL scalar types to Protocol Buffer types
10
- *
11
- * GraphQL has a smaller set of primitive types compared to Protocol Buffers.
12
- * This mapping ensures consistent representation between the two type systems.
13
- */
14
- const SCALAR_TYPE_MAP = {
15
- ID: 'string', // GraphQL IDs map to Proto strings
16
- String: 'string', // Direct mapping
17
- Int: 'int32', // GraphQL Int is 32-bit signed
18
- Float: 'double', // Using double for GraphQL Float gives better precision
19
- Boolean: 'bool', // Direct mapping
20
- };
21
- /**
22
- * Maps GraphQL scalar types to Protocol Buffer wrapper types for nullable fields
23
- *
24
- * These wrapper types allow distinguishing between unset fields and zero values
25
- * in Protocol Buffers, which is important for GraphQL nullable semantics.
26
- */
27
- const SCALAR_WRAPPER_TYPE_MAP = {
28
- ID: 'google.protobuf.StringValue',
29
- String: 'google.protobuf.StringValue',
30
- Int: 'google.protobuf.Int32Value',
31
- Float: 'google.protobuf.DoubleValue',
32
- Boolean: 'google.protobuf.BoolValue',
33
- };
7
+ import { buildProtoMessage, createNestedListWrapper, formatComment, getProtoTypeFromGraphQL, listNameByNestingLevel, renderRPCMethod, } from './proto-utils.js';
8
+ import { RequiredFieldsVisitor } from './required-fields-visitor.js';
34
9
  /**
35
10
  * Visitor that converts GraphQL SDL to Protocol Buffer text definition
36
11
  *
@@ -77,7 +52,7 @@ export class GraphQLToProtoTextVisitor {
77
52
  * This maintains field numbers even when fields are removed from the schema
78
53
  */
79
54
  this.fieldNumbersMap = {};
80
- const { serviceName = 'DefaultService', packageName = 'service.v1', lockData, includeComments = true } = options;
55
+ const { serviceName = 'DefaultService', packageName = 'service.v1', lockData, includeComments = true, protoOptions, ...languageOptions } = options;
81
56
  this.schema = schema;
82
57
  this.serviceName = serviceName;
83
58
  this.packageName = packageName;
@@ -89,27 +64,117 @@ export class GraphQLToProtoTextVisitor {
89
64
  }
90
65
  // Process language-specific proto options using buildProtoOptions
91
66
  const protoOptionsFromLanguageProps = buildProtoOptions({
92
- goPackage: options.goPackage,
93
- javaPackage: options.javaPackage,
94
- javaOuterClassname: options.javaOuterClassname,
95
- javaMultipleFiles: options.javaMultipleFiles,
96
- csharpNamespace: options.csharpNamespace,
97
- rubyPackage: options.rubyPackage,
98
- phpNamespace: options.phpNamespace,
99
- phpMetadataNamespace: options.phpMetadataNamespace,
100
- objcClassPrefix: options.objcClassPrefix,
101
- swiftPrefix: options.swiftPrefix,
67
+ ...languageOptions,
102
68
  }, packageName);
103
69
  // Add language-specific options
104
70
  if (protoOptionsFromLanguageProps.length > 0) {
105
71
  this.options.push(...protoOptionsFromLanguageProps);
106
72
  }
107
73
  // Process custom protoOptions array (for backward compatibility)
108
- if (options.protoOptions && options.protoOptions.length > 0) {
109
- const processedOptions = options.protoOptions.map((opt) => `option ${opt.name} = ${opt.constant};`);
74
+ if (protoOptions && protoOptions.length > 0) {
75
+ const processedOptions = protoOptions.map((opt) => `option ${opt.name} = ${opt.constant};`);
110
76
  this.options.push(...processedOptions);
111
77
  }
112
78
  }
79
+ /**
80
+ * Visit the GraphQL schema to generate Proto buffer definition
81
+ *
82
+ * @returns The complete Protocol Buffer definition as a string
83
+ */
84
+ visit() {
85
+ // Collect RPC methods and message definitions from all sources
86
+ const resolverResult = this.collectResolverRpcMethods();
87
+ const requiredFieldResult = this.collectRequiredFieldRpcMethods();
88
+ const entityResult = this.collectEntityRpcMethods();
89
+ const queryResult = this.collectQueryRpcMethods();
90
+ const mutationResult = this.collectMutationRpcMethods();
91
+ // Combine all RPC methods and message definitions
92
+ const allRpcMethods = [
93
+ ...entityResult.rpcMethods,
94
+ ...queryResult.rpcMethods,
95
+ ...mutationResult.rpcMethods,
96
+ ...resolverResult.rpcMethods,
97
+ ...requiredFieldResult.rpcMethods,
98
+ ];
99
+ const allMethodNames = [
100
+ ...entityResult.methodNames,
101
+ ...queryResult.methodNames,
102
+ ...mutationResult.methodNames,
103
+ ...resolverResult.methodNames,
104
+ ...requiredFieldResult.methodNames,
105
+ ];
106
+ const allMessageDefinitions = [
107
+ ...entityResult.messageDefinitions,
108
+ ...queryResult.messageDefinitions,
109
+ ...mutationResult.messageDefinitions,
110
+ ...resolverResult.messageDefinitions,
111
+ ...requiredFieldResult.messageDefinitions,
112
+ ];
113
+ // Add all types from the schema to the queue that weren't already queued
114
+ this.queueAllSchemaTypes();
115
+ // Process all complex types from the message queue to determine if wrapper types are needed
116
+ this.processMessageQueue();
117
+ // Add wrapper import if needed
118
+ if (this.usesWrapperTypes) {
119
+ this.addImport('google/protobuf/wrappers.proto');
120
+ }
121
+ // Build the complete proto file
122
+ let protoContent = [];
123
+ // Add the header (syntax, package, imports, options)
124
+ protoContent.push(...this.buildProtoHeader());
125
+ // Add a service description comment
126
+ if (this.includeComments) {
127
+ const serviceComment = `Service definition for ${this.serviceName}`;
128
+ protoContent.push(...this.formatComment(serviceComment, 0)); // Top-level comment, no indent
129
+ }
130
+ // Add service block containing RPC methods
131
+ protoContent.push(`service ${this.serviceName} {`);
132
+ this.indent++;
133
+ // Sort method names deterministically by alphabetical order
134
+ const orderedMethodNames = [...allMethodNames].sort();
135
+ // Add RPC methods in the ordered sequence
136
+ for (const methodName of orderedMethodNames) {
137
+ const methodIndex = allMethodNames.indexOf(methodName);
138
+ if (methodIndex !== -1) {
139
+ // Handle multi-line RPC definitions that include comments
140
+ const rpcMethodText = allRpcMethods[methodIndex];
141
+ if (rpcMethodText.includes('\n')) {
142
+ // For multi-line RPC method definitions (with comments), add each line separately
143
+ const lines = rpcMethodText.split('\n');
144
+ protoContent.push(...lines);
145
+ }
146
+ else {
147
+ protoContent.push(`${rpcMethodText}`);
148
+ }
149
+ }
150
+ }
151
+ // Close service definition
152
+ this.indent--;
153
+ protoContent.push('}');
154
+ protoContent.push('');
155
+ // Add all wrapper messages first since they might be referenced by other messages
156
+ if (this.nestedListWrappers.size > 0) {
157
+ // Sort the wrappers by name for deterministic output
158
+ const sortedWrapperNames = [...this.nestedListWrappers.keys()].sort();
159
+ for (const wrapperName of sortedWrapperNames) {
160
+ protoContent.push(this.nestedListWrappers.get(wrapperName));
161
+ }
162
+ }
163
+ // Add all message definitions
164
+ for (const messageDef of allMessageDefinitions) {
165
+ protoContent.push(messageDef);
166
+ }
167
+ protoContent = this.trimEmptyLines(protoContent);
168
+ this.protoText = this.trimEmptyLines(this.protoText);
169
+ if (this.protoText.length > 0) {
170
+ protoContent.push('');
171
+ }
172
+ // Add all processed types from protoText (populated by processMessageQueue)
173
+ protoContent.push(...this.protoText);
174
+ // Store the generated lock data for retrieval
175
+ this.generatedLockData = this.lockManager.getLockData();
176
+ return protoContent.join('\n');
177
+ }
113
178
  /**
114
179
  * Initialize the field numbers map from the lock data to preserve field numbers
115
180
  * even when fields are removed and later re-added
@@ -303,102 +368,6 @@ export class GraphQLToProtoTextVisitor {
303
368
  }
304
369
  return header;
305
370
  }
306
- /**
307
- * Visit the GraphQL schema to generate Proto buffer definition
308
- *
309
- * @returns The complete Protocol Buffer definition as a string
310
- */
311
- visit() {
312
- // Collect RPC methods and message definitions from all sources
313
- const resolverResult = this.collectResolverRpcMethods();
314
- const entityResult = this.collectEntityRpcMethods();
315
- const queryResult = this.collectQueryRpcMethods();
316
- const mutationResult = this.collectMutationRpcMethods();
317
- // Combine all RPC methods and message definitions
318
- const allRpcMethods = [
319
- ...entityResult.rpcMethods,
320
- ...queryResult.rpcMethods,
321
- ...mutationResult.rpcMethods,
322
- ...resolverResult.rpcMethods,
323
- ];
324
- const allMethodNames = [
325
- ...entityResult.methodNames,
326
- ...queryResult.methodNames,
327
- ...mutationResult.methodNames,
328
- ...resolverResult.methodNames,
329
- ];
330
- const allMessageDefinitions = [
331
- ...entityResult.messageDefinitions,
332
- ...queryResult.messageDefinitions,
333
- ...mutationResult.messageDefinitions,
334
- ...resolverResult.messageDefinitions,
335
- ];
336
- // Add all types from the schema to the queue that weren't already queued
337
- this.queueAllSchemaTypes();
338
- // Process all complex types from the message queue to determine if wrapper types are needed
339
- this.processMessageQueue();
340
- // Add wrapper import if needed
341
- if (this.usesWrapperTypes) {
342
- this.addImport('google/protobuf/wrappers.proto');
343
- }
344
- // Build the complete proto file
345
- let protoContent = [];
346
- // Add the header (syntax, package, imports, options)
347
- protoContent.push(...this.buildProtoHeader());
348
- // Add a service description comment
349
- if (this.includeComments) {
350
- const serviceComment = `Service definition for ${this.serviceName}`;
351
- protoContent.push(...this.formatComment(serviceComment, 0)); // Top-level comment, no indent
352
- }
353
- // Add service block containing RPC methods
354
- protoContent.push(`service ${this.serviceName} {`);
355
- this.indent++;
356
- // Sort method names deterministically by alphabetical order
357
- const orderedMethodNames = [...allMethodNames].sort();
358
- // Add RPC methods in the ordered sequence
359
- for (const methodName of orderedMethodNames) {
360
- const methodIndex = allMethodNames.indexOf(methodName);
361
- if (methodIndex !== -1) {
362
- // Handle multi-line RPC definitions that include comments
363
- const rpcMethodText = allRpcMethods[methodIndex];
364
- if (rpcMethodText.includes('\n')) {
365
- // For multi-line RPC method definitions (with comments), add each line separately
366
- const lines = rpcMethodText.split('\n');
367
- protoContent.push(...lines);
368
- }
369
- else {
370
- // For simple one-line RPC method definitions (ensure 2-space indentation)
371
- protoContent.push(` ${rpcMethodText}`);
372
- }
373
- }
374
- }
375
- // Close service definition
376
- this.indent--;
377
- protoContent.push('}');
378
- protoContent.push('');
379
- // Add all wrapper messages first since they might be referenced by other messages
380
- if (this.nestedListWrappers.size > 0) {
381
- // Sort the wrappers by name for deterministic output
382
- const sortedWrapperNames = Array.from(this.nestedListWrappers.keys()).sort();
383
- for (const wrapperName of sortedWrapperNames) {
384
- protoContent.push(this.nestedListWrappers.get(wrapperName));
385
- }
386
- }
387
- // Add all message definitions
388
- for (const messageDef of allMessageDefinitions) {
389
- protoContent.push(messageDef);
390
- }
391
- protoContent = this.trimEmptyLines(protoContent);
392
- this.protoText = this.trimEmptyLines(this.protoText);
393
- if (this.protoText.length > 0) {
394
- protoContent.push('');
395
- }
396
- // Add all processed types from protoText (populated by processMessageQueue)
397
- protoContent.push(...this.protoText);
398
- // Store the generated lock data for retrieval
399
- this.generatedLockData = this.lockManager.getLockData();
400
- return protoContent.join('\n');
401
- }
402
371
  /**
403
372
  * Trim empty lines from the beginning and end of the array
404
373
  */
@@ -438,44 +407,49 @@ export class GraphQLToProtoTextVisitor {
438
407
  typeName === ((_c = this.schema.getSubscriptionType()) === null || _c === void 0 ? void 0 : _c.name)) {
439
408
  continue;
440
409
  }
441
- // Check if this is an entity type (has @key directive)
442
- if (isObjectType(type)) {
443
- const keyDirectives = this.getKeyDirectives(type);
444
- if (keyDirectives.length > 0) {
445
- // Queue this type for message generation (only once)
446
- this.queueTypeForProcessing(type);
447
- // Normalize keys by sorting fields alphabetically and deduplicating
448
- const normalizedKeysSet = new Set();
449
- for (const keyDirective of keyDirectives) {
450
- const keyInfo = this.getKeyInfoFromDirective(keyDirective);
451
- if (!keyInfo)
452
- continue;
453
- const { keyString, resolvable } = keyInfo;
454
- if (!resolvable)
455
- continue;
456
- const normalizedKey = keyString
457
- .split(/[,\s]+/)
458
- .filter((field) => field.length > 0)
459
- .sort()
460
- .join(' ');
461
- normalizedKeysSet.add(normalizedKey);
462
- }
463
- // Process each normalized key
464
- for (const normalizedKeyString of normalizedKeysSet) {
465
- const methodName = createEntityLookupMethodName(typeName, normalizedKeyString);
466
- const requestName = createRequestMessageName(methodName);
467
- const responseName = createResponseMessageName(methodName);
468
- // Add method name and RPC method with description from the entity type
469
- result.methodNames.push(methodName);
470
- const keyFields = normalizedKeyString.split(' ');
471
- const keyDescription = keyFields.length === 1 ? keyFields[0] : keyFields.join(' and ');
472
- const description = `Lookup ${typeName} entity by ${keyDescription}${type.description ? ': ' + type.description : ''}`;
473
- result.rpcMethods.push(this.createRpcMethod(methodName, requestName, responseName, description));
474
- // Create request and response messages for this key combination
475
- result.messageDefinitions.push(...this.createKeyRequestMessage(typeName, requestName, normalizedKeyString, responseName));
476
- result.messageDefinitions.push(...this.createKeyResponseMessage(typeName, responseName, requestName));
477
- }
410
+ // Skip non-object types
411
+ if (!isObjectType(type)) {
412
+ continue;
413
+ }
414
+ const keyDirectives = this.getKeyDirectives(type);
415
+ // Skip types that don't have @key directives
416
+ if (keyDirectives.length === 0) {
417
+ continue;
418
+ }
419
+ // Queue this type for message generation (only once)
420
+ this.queueTypeForProcessing(type);
421
+ // Normalize keys by sorting fields alphabetically and deduplicating
422
+ const normalizedKeysSet = new Set();
423
+ for (const keyDirective of keyDirectives) {
424
+ const keyInfo = this.getKeyInfoFromDirective(keyDirective);
425
+ if (!keyInfo) {
426
+ continue;
427
+ }
428
+ const { keyString, resolvable } = keyInfo;
429
+ if (!resolvable) {
430
+ continue;
478
431
  }
432
+ const normalizedKey = keyString
433
+ .split(/[\s,]+/)
434
+ .filter((field) => field.length > 0)
435
+ .sort()
436
+ .join(' ');
437
+ normalizedKeysSet.add(normalizedKey);
438
+ }
439
+ // Process each normalized key
440
+ for (const normalizedKeyString of normalizedKeysSet) {
441
+ const methodName = createEntityLookupMethodName(typeName, normalizedKeyString);
442
+ const requestName = createRequestMessageName(methodName);
443
+ const responseName = createResponseMessageName(methodName);
444
+ // Add method name and RPC method with description from the entity type
445
+ result.methodNames.push(methodName);
446
+ const keyFields = normalizedKeyString.split(' ');
447
+ const keyDescription = keyFields.length === 1 ? keyFields[0] : keyFields.join(' and ');
448
+ const description = `Lookup ${typeName} entity by ${keyDescription}${type.description ? ': ' + type.description : ''}`;
449
+ result.rpcMethods.push(this.createRpcMethod(methodName, requestName, responseName, description));
450
+ // Create request and response messages for this key combination
451
+ result.messageDefinitions.push(...this.createKeyRequestMessage(typeName, requestName, normalizedKeyString, responseName));
452
+ result.messageDefinitions.push(...this.createKeyResponseMessage(typeName, responseName, requestName));
479
453
  }
480
454
  }
481
455
  return result;
@@ -503,18 +477,21 @@ export class GraphQLToProtoTextVisitor {
503
477
  const result = { rpcMethods: [], methodNames: [], messageDefinitions: [] };
504
478
  // Get the root operation type (Query or Mutation)
505
479
  const rootType = operationType === 'Query' ? this.schema.getQueryType() : this.schema.getMutationType();
506
- if (!rootType)
480
+ if (!rootType) {
507
481
  return result;
482
+ }
508
483
  const fields = rootType.getFields();
509
484
  // Get field names and order them using the lock manager
510
485
  const fieldNames = Object.keys(fields);
511
486
  const orderedFieldNames = this.lockManager.reconcileMessageFieldOrder(operationType, fieldNames);
512
487
  for (const fieldName of orderedFieldNames) {
513
488
  // Skip special fields like _entities
514
- if (fieldName === '_entities')
489
+ if (fieldName === '_entities' || fieldName === '_service') {
515
490
  continue;
516
- if (!fields[fieldName])
491
+ }
492
+ if (!fields[fieldName]) {
517
493
  continue;
494
+ }
518
495
  const field = fields[fieldName];
519
496
  const mappedName = createOperationMethodName(operationType, fieldName);
520
497
  const requestName = createRequestMessageName(mappedName);
@@ -557,13 +534,13 @@ export class GraphQLToProtoTextVisitor {
557
534
  * @returns The RPC method definition with or without comment
558
535
  */
559
536
  createRpcMethod(methodName, requestName, responseName, description) {
560
- if (!this.includeComments || !description) {
561
- return `rpc ${methodName}(${requestName}) returns (${responseName}) {}`;
562
- }
563
- // RPC method comments should be indented 1 level (2 spaces)
564
- const commentLines = this.formatComment(description, 1);
565
- const methodLine = ` rpc ${methodName}(${requestName}) returns (${responseName}) {}`;
566
- return [...commentLines, methodLine].join('\n');
537
+ const rpcLines = renderRPCMethod(this.includeComments, {
538
+ name: methodName,
539
+ request: requestName,
540
+ response: responseName,
541
+ description,
542
+ });
543
+ return rpcLines.join('\n');
567
544
  }
568
545
  /**
569
546
  * Creates a request message for entity lookup without adding to protoText
@@ -592,7 +569,7 @@ export class GraphQLToProtoTextVisitor {
592
569
  }
593
570
  // Add all key fields to the key message
594
571
  const protoKeyFields = [];
595
- keyFields.forEach((keyField, index) => {
572
+ for (const [index, keyField] of keyFields.entries()) {
596
573
  const protoKeyField = graphqlFieldToProtoField(keyField);
597
574
  protoKeyFields.push(protoKeyField);
598
575
  // Get the appropriate field number for this key field
@@ -602,7 +579,7 @@ export class GraphQLToProtoTextVisitor {
602
579
  messageLines.push(...this.formatComment(keyFieldComment, 1)); // Field comment, indent 1 level
603
580
  }
604
581
  messageLines.push(` string ${protoKeyField} = ${keyFieldNumber};`);
605
- });
582
+ }
606
583
  messageLines.push('}');
607
584
  messageLines.push('');
608
585
  // Ensure the key message is registered in the lock manager data
@@ -717,8 +694,9 @@ Example:
717
694
  // Process arguments in the order specified by the lock manager
718
695
  for (const argName of orderedArgNames) {
719
696
  const arg = field.args.find((a) => a.name === argName);
720
- if (!arg)
697
+ if (!arg) {
721
698
  continue;
699
+ }
722
700
  const argType = this.getProtoTypeFromGraphQL(arg.type);
723
701
  const argProtoName = graphqlFieldToProtoField(arg.name);
724
702
  const fieldNumber = this.getFieldNumber(requestName, argProtoName, this.getNextAvailableFieldNumber(requestName));
@@ -801,7 +779,16 @@ Example:
801
779
  */
802
780
  getKeyDirectives(type) {
803
781
  var _a, _b;
804
- return ((_b = (_a = type.astNode) === null || _a === void 0 ? void 0 : _a.directives) === null || _b === void 0 ? void 0 : _b.filter((d) => d.name.value === 'key')) || [];
782
+ return ((_b = (_a = type.astNode) === null || _a === void 0 ? void 0 : _a.directives) === null || _b === void 0 ? void 0 : _b.filter((d) => d.name.value === KEY_DIRECTIVE_NAME)) || [];
783
+ }
784
+ /**
785
+ * Checks if a type has a @key directive
786
+ * @param type - The type to check
787
+ * @returns True if the type has a @key directive, false otherwise
788
+ */
789
+ hasKeyDirective(type) {
790
+ var _a, _b, _c;
791
+ return (_c = (_b = (_a = type.astNode) === null || _a === void 0 ? void 0 : _a.directives) === null || _b === void 0 ? void 0 : _b.some((d) => d.name.value === KEY_DIRECTIVE_NAME)) !== null && _c !== void 0 ? _c : false;
805
792
  }
806
793
  /**
807
794
  * Extract key info from a directive
@@ -835,14 +822,14 @@ Example:
835
822
  collectResolverRpcMethods() {
836
823
  const typeMap = this.schema.getTypeMap();
837
824
  const result = { rpcMethods: [], methodNames: [], messageDefinitions: [] };
838
- Object.values(typeMap).forEach((type) => {
825
+ for (const type of Object.values(typeMap)) {
839
826
  if (!isObjectType(type) || this.isOperationType(type)) {
840
- return;
827
+ continue;
841
828
  }
842
829
  const fields = type.getFields();
843
- Object.values(fields).forEach((field) => {
830
+ for (const field of Object.values(fields)) {
844
831
  if (field.args.length === 0) {
845
- return;
832
+ continue;
846
833
  }
847
834
  const methodName = createResolverMethodName(type.name, field.name);
848
835
  const requestName = createRequestMessageName(methodName);
@@ -852,10 +839,38 @@ Example:
852
839
  result.rpcMethods.push(this.createRpcMethod(methodName, requestName, responseName, field.description));
853
840
  result.messageDefinitions.push(...this.createResolverRequestMessage(methodName, requestName, type, field));
854
841
  result.messageDefinitions.push(...this.createResolverResponseMessage(methodName, responseName, field));
855
- });
856
- });
842
+ }
843
+ }
857
844
  return result;
858
845
  }
846
+ collectRequiredFieldRpcMethods() {
847
+ const typeMap = this.schema.getTypeMap();
848
+ const result = { rpcMethods: [], methodNames: [], messageDefinitions: [] };
849
+ const entityTypes = Object.values(typeMap).filter((type) => this.isEntityType(type));
850
+ for (const entity of entityTypes) {
851
+ const requiredFields = Object.values(entity.getFields()).filter((field) => { var _a, _b; return (_b = (_a = field.astNode) === null || _a === void 0 ? void 0 : _a.directives) === null || _b === void 0 ? void 0 : _b.some((d) => d.name.value === REQUIRES_DIRECTIVE_NAME); });
852
+ for (const requiredField of requiredFields) {
853
+ const fieldSet = this.getRequiredFieldSet(requiredField);
854
+ const visitor = new RequiredFieldsVisitor(this.schema, entity, requiredField, fieldSet);
855
+ visitor.visit();
856
+ const rpcMethods = visitor.getRPCMethods();
857
+ const messageDefinitions = visitor.getMessageDefinitions();
858
+ result.rpcMethods.push(...rpcMethods.map((m) => renderRPCMethod(this.includeComments, m).join('\n')));
859
+ result.methodNames.push(...rpcMethods.map((m) => m.name));
860
+ const messageLines = messageDefinitions.flatMap((m) => buildProtoMessage(this.includeComments, m));
861
+ result.messageDefinitions.push(...messageLines);
862
+ }
863
+ }
864
+ return result;
865
+ }
866
+ getRequiredFieldSet(field) {
867
+ var _a, _b, _c, _d, _e;
868
+ const node = (_e = (_d = (_c = (_b = (_a = field.astNode) === null || _a === void 0 ? void 0 : _a.directives) === null || _b === void 0 ? void 0 : _b.find((d) => d.name.value === REQUIRES_DIRECTIVE_NAME)) === null || _c === void 0 ? void 0 : _c.arguments) === null || _d === void 0 ? void 0 : _d.find((arg) => arg.name.value === 'fields')) === null || _e === void 0 ? void 0 : _e.value;
869
+ if (!node) {
870
+ throw new Error(`Required field set not found for field ${field.name}`);
871
+ }
872
+ return node.value;
873
+ }
859
874
  getFieldContext(parent, field) {
860
875
  var _a, _b;
861
876
  const resolvedDirective = this.findResolverDirective(field);
@@ -868,15 +883,18 @@ Example:
868
883
  }
869
884
  const idFields = this.getIDFields(parent, field.name);
870
885
  switch (idFields.length) {
871
- case 0:
886
+ case 0: {
872
887
  return { context: '', error: 'No fields with type ID found' };
873
- case 1:
888
+ }
889
+ case 1: {
874
890
  return { context: idFields[0].name, error: undefined };
875
- default:
891
+ }
892
+ default: {
876
893
  return {
877
894
  context: '',
878
895
  error: `Multiple fields with type ID found - provide a context with the fields you want to use in the @${CONNECT_FIELD_RESOLVER} directive`,
879
896
  };
897
+ }
880
898
  }
881
899
  }
882
900
  getIDFields(type, currentFieldName) {
@@ -950,7 +968,7 @@ Example:
950
968
  })),
951
969
  }));
952
970
  // filter the fields in the parent type that are in the context
953
- const searchFields = context.split(/[,\s]+/).filter((field) => field.length > 0);
971
+ const searchFields = context.split(/[\s,]+/).filter((field) => field.length > 0);
954
972
  const fieldFilter = Object.values(parent.getFields()).filter((field) => searchFields.includes(field.name));
955
973
  if (searchFields.length !== fieldFilter.length) {
956
974
  throw new Error(`Invalid field context for resolver. Could not find all fields in the parent type: ${context}`);
@@ -966,7 +984,7 @@ Example:
966
984
  })),
967
985
  }));
968
986
  fieldNumber = 0;
969
- let keyMessageFields = [];
987
+ const keyMessageFields = [];
970
988
  // add the context message to the key message
971
989
  keyMessageFields.push({
972
990
  fieldName: CONTEXT,
@@ -1047,6 +1065,14 @@ Example:
1047
1065
  }
1048
1066
  return type.name === 'Query' || type.name === 'Mutation' || type.name === 'Subscription';
1049
1067
  }
1068
+ /**
1069
+ * Checks if a type is an entity type
1070
+ * @param type - The type to check
1071
+ * @returns True if the type is an entity type, false otherwise
1072
+ */
1073
+ isEntityType(type) {
1074
+ return isObjectType(type) && !this.isOperationType(type) && this.hasKeyDirective(type);
1075
+ }
1050
1076
  /**
1051
1077
  * Queue all types from the schema that need processing
1052
1078
  */
@@ -1125,12 +1151,19 @@ Example:
1125
1151
  this.processedTypes.add(type.name);
1126
1152
  return;
1127
1153
  }
1128
- const fieldsWithoutArguments = Object.values(type.getFields()).filter((field) => field.args.length === 0);
1154
+ const allFields = Object.values(type.getFields());
1155
+ // Filter out fields that have arguments as those are handled in separate resolver rpcs
1156
+ const validFields = allFields
1157
+ .filter((field) => field.args.length === 0)
1158
+ .filter((field) => {
1159
+ var _a, _b;
1160
+ return !((_b = (_a = field.astNode) === null || _a === void 0 ? void 0 : _a.directives) === null || _b === void 0 ? void 0 : _b.some((directive) => directive.name.value === EXTERNAL_DIRECTIVE_NAME || directive.name.value === REQUIRES_DIRECTIVE_NAME));
1161
+ });
1129
1162
  // Check for field removals if lock data exists for this type
1130
1163
  const lockData = this.lockManager.getLockData();
1131
1164
  if (lockData.messages[type.name]) {
1132
1165
  const originalFieldNames = Object.keys(lockData.messages[type.name].fields);
1133
- const currentFieldNames = fieldsWithoutArguments.map((field) => field.name);
1166
+ const currentFieldNames = validFields.map((field) => field.name);
1134
1167
  this.trackRemovedFields(type.name, originalFieldNames, currentFieldNames);
1135
1168
  }
1136
1169
  this.protoText.push('');
@@ -1147,11 +1180,12 @@ Example:
1147
1180
  }
1148
1181
  const fields = type.getFields();
1149
1182
  // Get field names and order them using the lock manager
1150
- const fieldNames = Object.keys(fields);
1183
+ const fieldNames = Object.keys(fields).filter((fieldName) => validFields.some((field) => field.name === fieldName));
1151
1184
  const orderedFieldNames = this.lockManager.reconcileMessageFieldOrder(type.name, fieldNames);
1152
1185
  for (const fieldName of orderedFieldNames) {
1153
- if (!fields[fieldName])
1186
+ if (!fields[fieldName]) {
1154
1187
  continue;
1188
+ }
1155
1189
  // ignore fields with arguments as those are handled in separate resolver rpcs
1156
1190
  const field = fields[fieldName];
1157
1191
  if (field.args.length > 0) {
@@ -1268,8 +1302,9 @@ Example:
1268
1302
  const fieldNames = Object.keys(fields);
1269
1303
  const orderedFieldNames = this.lockManager.reconcileMessageFieldOrder(type.name, fieldNames);
1270
1304
  for (const fieldName of orderedFieldNames) {
1271
- if (!fields[fieldName])
1305
+ if (!fields[fieldName]) {
1272
1306
  continue;
1307
+ }
1273
1308
  const field = fields[fieldName];
1274
1309
  const fieldType = this.getProtoTypeFromGraphQL(field.type);
1275
1310
  const protoFieldName = graphqlFieldToProtoField(fieldName);
@@ -1315,7 +1350,7 @@ Example:
1315
1350
  // Mark the interface as processed to avoid infinite recursion
1316
1351
  this.processedTypes.add(type.name);
1317
1352
  const implementingTypes = Object.values(this.schema.getTypeMap())
1318
- .filter(isObjectType)
1353
+ .filter((t) => isObjectType(t))
1319
1354
  .filter((t) => t.getInterfaces().some((i) => i.name === type.name));
1320
1355
  if (implementingTypes.length === 0) {
1321
1356
  // No implementing types, just create a regular message
@@ -1335,11 +1370,11 @@ Example:
1335
1370
  // Use lock manager to order implementing types
1336
1371
  const typeNames = implementingTypes.map((t) => t.name);
1337
1372
  const orderedTypeNames = this.lockManager.reconcileMessageFieldOrder(`${type.name}Implementations`, typeNames);
1338
- for (let i = 0; i < orderedTypeNames.length; i++) {
1339
- const typeName = orderedTypeNames[i];
1373
+ for (const [i, typeName] of orderedTypeNames.entries()) {
1340
1374
  const implType = implementingTypes.find((t) => t.name === typeName);
1341
- if (!implType)
1375
+ if (!implType) {
1342
1376
  continue;
1377
+ }
1343
1378
  // Add implementing type description as comment if available
1344
1379
  if (implType.description) {
1345
1380
  this.protoText.push(...this.formatComment(implType.description, 1)); // Field comment, indent 1 level
@@ -1383,11 +1418,11 @@ Example:
1383
1418
  const types = type.getTypes();
1384
1419
  const typeNames = types.map((t) => t.name);
1385
1420
  const orderedTypeNames = this.lockManager.reconcileMessageFieldOrder(`${type.name}Members`, typeNames);
1386
- for (let i = 0; i < orderedTypeNames.length; i++) {
1387
- const typeName = orderedTypeNames[i];
1421
+ for (const [i, typeName] of orderedTypeNames.entries()) {
1388
1422
  const memberType = types.find((t) => t.name === typeName);
1389
- if (!memberType)
1423
+ if (!memberType) {
1390
1424
  continue;
1425
+ }
1391
1426
  // Add member type description as comment if available
1392
1427
  if (memberType.description) {
1393
1428
  this.protoText.push(...this.formatComment(memberType.description, 1)); // Field comment, indent 1 level
@@ -1412,7 +1447,6 @@ Example:
1412
1447
  * @param type - The GraphQL enum type
1413
1448
  */
1414
1449
  processEnumType(type) {
1415
- var _a, _b;
1416
1450
  // Check for enum value removals if lock data exists for this enum
1417
1451
  const lockData = this.lockManager.getLockData();
1418
1452
  if (lockData.enums[type.name]) {
@@ -1435,33 +1469,36 @@ Example:
1435
1469
  // Add unspecified value as first enum value (required in proto3)
1436
1470
  const unspecifiedValue = createEnumUnspecifiedValue(type.name);
1437
1471
  this.protoText.push(` ${unspecifiedValue} = 0;`);
1438
- // Use lock manager to order enum values
1472
+ // Use lock manager to order enum values, filtering out any value whose proto name
1473
+ // collides with the auto-generated UNSPECIFIED value (already emitted at position 0)
1439
1474
  const values = type.getValues();
1440
- const valueNames = values.map((v) => v.name);
1475
+ const valueNames = values
1476
+ .filter((v) => graphqlEnumValueToProtoEnumValue(type.name, v.name) !== unspecifiedValue)
1477
+ .map((v) => v.name);
1441
1478
  const orderedValueNames = this.lockManager.reconcileEnumValueOrder(type.name, valueNames);
1442
1479
  for (const valueName of orderedValueNames) {
1443
1480
  const value = values.find((v) => v.name === valueName);
1444
- if (!value)
1481
+ if (!value) {
1445
1482
  continue;
1483
+ }
1446
1484
  const protoEnumValue = graphqlEnumValueToProtoEnumValue(type.name, value.name);
1447
1485
  const deprecationInfo = this.enumValueIsDeprecated(value);
1448
1486
  // Add enum value description as comment
1449
1487
  if (value.description) {
1450
1488
  this.protoText.push(...this.formatComment(value.description, 1)); // Field comment, indent 1 level
1451
1489
  }
1452
- if (deprecationInfo.deprecated && ((_b = (_a = deprecationInfo.reason) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0) {
1490
+ if (deprecationInfo.deprecated && deprecationInfo.reason && deprecationInfo.reason.length > 0) {
1453
1491
  this.protoText.push(...this.formatComment(`Deprecation notice: ${deprecationInfo.reason}`, 1));
1454
1492
  }
1455
1493
  // Get value number from lock data
1456
- const lockData = this.lockManager.getLockData();
1457
1494
  let valueNumber = 0;
1458
1495
  if (lockData.enums[type.name] && lockData.enums[type.name].fields[value.name]) {
1459
1496
  valueNumber = lockData.enums[type.name].fields[value.name];
1460
1497
  }
1461
1498
  else {
1462
- // This should never happen since we just reconciled, but just in case
1463
- console.warn(`Missing enum value number for ${type.name}.${value.name}`);
1464
- continue;
1499
+ // Indicates a bug in lock reconciliation; fail fast rather than silently
1500
+ // producing a proto enum value with number 0, which would collide with UNSPECIFIED.
1501
+ throw new Error(`Missing enum value number for ${type.name}.${value.name}`);
1465
1502
  }
1466
1503
  const fieldOptions = [];
1467
1504
  if (deprecationInfo.deprecated) {
@@ -1484,142 +1521,23 @@ Example:
1484
1521
  * @returns The corresponding Protocol Buffer type name
1485
1522
  */
1486
1523
  getProtoTypeFromGraphQL(graphqlType, ignoreWrapperTypes = false) {
1487
- // Nullable lists need to be handled first, otherwise they will be treated as scalar types
1488
- if (isListType(graphqlType) || (isNonNullType(graphqlType) && isListType(graphqlType.ofType))) {
1489
- return this.handleListType(graphqlType);
1490
- }
1491
- // For nullable scalar types, use wrapper types
1492
- if (isScalarType(graphqlType)) {
1493
- if (ignoreWrapperTypes) {
1494
- return { typeName: SCALAR_TYPE_MAP[graphqlType.name] || 'string', isRepeated: false };
1495
- }
1496
- this.usesWrapperTypes = true; // Track that we're using wrapper types
1497
- return {
1498
- typeName: SCALAR_WRAPPER_TYPE_MAP[graphqlType.name] || 'google.protobuf.StringValue',
1499
- isRepeated: false,
1500
- };
1501
- }
1502
- if (isEnumType(graphqlType)) {
1503
- return { typeName: graphqlType.name, isRepeated: false };
1504
- }
1505
- if (isNonNullType(graphqlType)) {
1506
- // For non-null scalar types, use the base type
1507
- if (isScalarType(graphqlType.ofType)) {
1508
- return { typeName: SCALAR_TYPE_MAP[graphqlType.ofType.name] || 'string', isRepeated: false };
1524
+ const protoFieldType = getProtoTypeFromGraphQL(this.includeComments, graphqlType, ignoreWrapperTypes);
1525
+ if (!ignoreWrapperTypes && protoFieldType.isWrapper) {
1526
+ this.usesWrapperTypes = true;
1527
+ }
1528
+ const listWrapper = protoFieldType.listWrapper;
1529
+ if (listWrapper) {
1530
+ for (let i = 1; i <= listWrapper.nestingLevel; i++) {
1531
+ const wrapperName = listNameByNestingLevel(i, listWrapper.baseType);
1532
+ if (this.processedTypes.has(wrapperName) || this.nestedListWrappers.has(wrapperName)) {
1533
+ continue;
1534
+ }
1535
+ const wrapperMessage = createNestedListWrapper(this.includeComments, i, listWrapper.baseType);
1536
+ this.nestedListWrappers.set(wrapperName, wrapperMessage);
1537
+ this.processedTypes.add(wrapperName);
1509
1538
  }
1510
- return this.getProtoTypeFromGraphQL(graphqlType.ofType);
1511
- }
1512
- // Named types (object, interface, union, input)
1513
- const namedType = graphqlType;
1514
- if (namedType && typeof namedType.name === 'string') {
1515
- return { typeName: namedType.name, isRepeated: false };
1516
- }
1517
- return { typeName: 'string', isRepeated: false }; // Default fallback
1518
- }
1519
- /**
1520
- * Converts GraphQL list types to appropriate Protocol Buffer representations.
1521
- *
1522
- * For non-nullable, single-level lists (e.g., [String!]!), generates simple repeated fields.
1523
- * For nullable lists (e.g., [String]) or nested lists (e.g., [[String]]), creates wrapper
1524
- * messages to properly handle nullability in proto3.
1525
- *
1526
- * Examples:
1527
- * - [String!]! → repeated string field_name = 1;
1528
- * - [String] → ListOfString field_name = 1; (with wrapper message)
1529
- * - [[String!]!]! → ListOfListOfString field_name = 1; (with nested wrapper messages)
1530
- * - [[String]] → ListOfListOfString field_name = 1; (with nested wrapper messages)
1531
- *
1532
- * @param graphqlType - The GraphQL list type to convert
1533
- * @returns ProtoType object containing the type name and whether it should be repeated
1534
- */
1535
- handleListType(graphqlType) {
1536
- const listType = unwrapNonNullType(graphqlType);
1537
- const isNullableList = !isNonNullType(graphqlType);
1538
- const isNested = isNestedListType(listType);
1539
- // Simple non-nullable lists can use repeated fields directly
1540
- if (!isNullableList && !isNested) {
1541
- return { ...this.getProtoTypeFromGraphQL(getNamedType(listType), true), isRepeated: true };
1542
- }
1543
- // Nullable or nested lists need wrapper messages
1544
- const baseType = getNamedType(listType);
1545
- const nestingLevel = calculateNestingLevel(listType);
1546
- // For nested lists, always use full nesting level to preserve inner list nullability
1547
- // For single-level nullable lists, use nesting level 1
1548
- const wrapperNestingLevel = isNested ? nestingLevel : 1;
1549
- // Generate all required wrapper messages
1550
- let wrapperName = '';
1551
- for (let i = 1; i <= wrapperNestingLevel; i++) {
1552
- wrapperName = this.createNestedListWrapper(i, baseType);
1553
- }
1554
- // For nested lists, never use repeated at field level to preserve nullability
1555
- return { typeName: wrapperName, isRepeated: false };
1556
- }
1557
- /**
1558
- * Creates wrapper messages for nullable or nested GraphQL lists.
1559
- *
1560
- * Generates Protocol Buffer message definitions to handle list nullability and nesting.
1561
- * The wrapper messages are stored and later included in the final proto output.
1562
- *
1563
- * For level 1: Creates simple wrapper like:
1564
- * message ListOfString {
1565
- * repeated string items = 1;
1566
- * }
1567
- *
1568
- * For level > 1: Creates nested wrapper structures like:
1569
- * message ListOfListOfString {
1570
- * message List {
1571
- * repeated ListOfString items = 1;
1572
- * }
1573
- * List list = 1;
1574
- * }
1575
- *
1576
- * @param level - The nesting level (1 for simple wrapper, >1 for nested structures)
1577
- * @param baseType - The GraphQL base type being wrapped (e.g., String, User, etc.)
1578
- * @returns The generated wrapper message name (e.g., "ListOfString", "ListOfListOfUser")
1579
- */
1580
- createNestedListWrapper(level, baseType) {
1581
- const wrapperName = `${'ListOf'.repeat(level)}${baseType.name}`;
1582
- // Return existing wrapper if already created
1583
- if (this.processedTypes.has(wrapperName) || this.nestedListWrappers.has(wrapperName)) {
1584
- return wrapperName;
1585
- }
1586
- this.processedTypes.add(wrapperName);
1587
- const messageLines = this.buildWrapperMessage(wrapperName, level, baseType);
1588
- this.nestedListWrappers.set(wrapperName, messageLines.join('\n'));
1589
- return wrapperName;
1590
- }
1591
- /**
1592
- * Builds the message lines for a wrapper message
1593
- */
1594
- buildWrapperMessage(wrapperName, level, baseType) {
1595
- const lines = [];
1596
- // Add comment if enabled
1597
- if (this.includeComments) {
1598
- lines.push(...this.formatComment(`Wrapper message for a list of ${baseType.name}.`, 0));
1599
- }
1600
- const formatIndent = (indent, content) => {
1601
- return ' '.repeat(indent) + content;
1602
- };
1603
- lines.push(`message ${wrapperName} {`);
1604
- let innerWrapperName = '';
1605
- if (level > 1) {
1606
- innerWrapperName = `${'ListOf'.repeat(level - 1)}${baseType.name}`;
1607
- }
1608
- else {
1609
- innerWrapperName = this.getProtoTypeFromGraphQL(baseType, true).typeName;
1610
1539
  }
1611
- lines.push(formatIndent(1, `message List {`), formatIndent(2, `repeated ${innerWrapperName} items = 1;`), formatIndent(1, `}`), formatIndent(1, `List list = 1;`), formatIndent(0, `}`));
1612
- return lines;
1613
- }
1614
- /**
1615
- * Get indentation based on the current level
1616
- *
1617
- * Helper method to maintain consistent indentation in the output.
1618
- *
1619
- * @returns String with spaces for the current indentation level
1620
- */
1621
- getIndent() {
1622
- return ' '.repeat(this.indent);
1540
+ return protoFieldType;
1623
1541
  }
1624
1542
  /**
1625
1543
  * Get the generated lock data after visiting
@@ -1639,8 +1557,9 @@ Example:
1639
1557
  * @returns A formatted string for the reserved statement
1640
1558
  */
1641
1559
  formatReservedNumbers(numbers) {
1642
- if (numbers.length === 0)
1560
+ if (numbers.length === 0) {
1643
1561
  return '';
1562
+ }
1644
1563
  // Sort numbers for better readability
1645
1564
  const sortedNumbers = [...numbers].sort((a, b) => a - b);
1646
1565
  // Simple case: only one number
@@ -1668,12 +1587,7 @@ Example:
1668
1587
  // Format the ranges
1669
1588
  return ranges
1670
1589
  .map(([start, end]) => {
1671
- if (start === end) {
1672
- return start.toString();
1673
- }
1674
- else {
1675
- return `${start} to ${end}`;
1676
- }
1590
+ return start === end ? start.toString() : `${start} to ${end}`;
1677
1591
  })
1678
1592
  .join(', ');
1679
1593
  }
@@ -1684,18 +1598,7 @@ Example:
1684
1598
  * @returns Array of comment lines with proper indentation
1685
1599
  */
1686
1600
  formatComment(description, indentLevel = 0) {
1687
- if (!this.includeComments || !description) {
1688
- return [];
1689
- }
1690
- // Use 2-space indentation consistently
1691
- const indent = ' '.repeat(indentLevel);
1692
- const lines = description.trim().split('\n');
1693
- if (lines.length === 1) {
1694
- return [`${indent}// ${lines[0]}`];
1695
- }
1696
- else {
1697
- return [`${indent}/*`, ...lines.map((line) => `${indent} * ${line}`), `${indent} */`];
1698
- }
1601
+ return formatComment(this.includeComments, description, indentLevel);
1699
1602
  }
1700
1603
  /**
1701
1604
  * Builds a message definition from a ProtoMessage object
@@ -1703,29 +1606,7 @@ Example:
1703
1606
  * @returns The message definition
1704
1607
  */
1705
1608
  buildMessage(message) {
1706
- const messageLines = this.formatComment(message.description, 0);
1707
- messageLines.push(`message ${message.messageName} {`);
1708
- if (message.reservedNumbers && message.reservedNumbers.length > 0) {
1709
- messageLines.push(this.formatIndent(1, `reserved ${message.reservedNumbers};`));
1710
- }
1711
- message.fields.forEach((field) => {
1712
- if (field.description) {
1713
- messageLines.push(...this.formatComment(field.description, 1));
1714
- }
1715
- let repeated = field.isRepeated ? 'repeated ' : '';
1716
- messageLines.push(this.formatIndent(1, `${repeated}${field.typeName} ${field.fieldName} = ${field.fieldNumber};`));
1717
- });
1718
- messageLines.push('}', '');
1719
- return messageLines;
1720
- }
1721
- /**
1722
- * Formats the indent for the content
1723
- * @param indent - The indent level
1724
- * @param content - The content to format
1725
- * @returns The formatted content
1726
- */
1727
- formatIndent(indent, content) {
1728
- return ' '.repeat(indent) + content;
1609
+ return buildProtoMessage(this.includeComments, message);
1729
1610
  }
1730
1611
  }
1731
1612
  //# sourceMappingURL=sdl-to-proto-visitor.js.map