@wundergraph/protographic 0.1.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.
@@ -0,0 +1,1200 @@
1
+ import { getNamedType, isEnumType, isInputObjectType, isInterfaceType, isListType, isNonNullType, isObjectType, isScalarType, isUnionType, } from 'graphql';
2
+ import { createEntityLookupMethodName, createEntityLookupRequestName, createEntityLookupResponseName, createEnumUnspecifiedValue, createOperationMethodName, createRequestMessageName, createResponseMessageName, graphqlEnumValueToProtoEnumValue, graphqlFieldToProtoField, } from './naming-conventions.js';
3
+ import { camelCase } from 'lodash-es';
4
+ import { ProtoLockManager } from './proto-lock.js';
5
+ /**
6
+ * Maps GraphQL scalar types to Protocol Buffer types
7
+ *
8
+ * GraphQL has a smaller set of primitive types compared to Protocol Buffers.
9
+ * This mapping ensures consistent representation between the two type systems.
10
+ */
11
+ const SCALAR_TYPE_MAP = {
12
+ ID: 'string', // GraphQL IDs map to Proto strings
13
+ String: 'string', // Direct mapping
14
+ Int: 'int32', // GraphQL Int is 32-bit signed
15
+ Float: 'double', // Using double for GraphQL Float gives better precision
16
+ Boolean: 'bool', // Direct mapping
17
+ };
18
+ /**
19
+ * Visitor that converts GraphQL SDL to Protocol Buffer text definition
20
+ *
21
+ * This visitor traverses a GraphQL schema and generates a Protocol Buffer
22
+ * service and message definitions. It handles:
23
+ *
24
+ * 1. GraphQL scalars, objects, interfaces, unions, and enums
25
+ * 2. Federation entity types with @key directives
26
+ * 3. Query and Mutation operations as RPC methods
27
+ * 4. Field and argument mappings with proper naming conventions
28
+ * 5. Comments/descriptions from GraphQL types and fields
29
+ *
30
+ * The visitor uses a queue-based approach to resolve dependencies between
31
+ * types and ensure all referenced types are processed.
32
+ */
33
+ export class GraphQLToProtoTextVisitor {
34
+ /**
35
+ * Creates a new visitor to convert a GraphQL schema to Protocol Buffers
36
+ *
37
+ * @param schema - The GraphQL schema to convert
38
+ * @param options - Configuration options for the visitor
39
+ */
40
+ constructor(schema, options = {}) {
41
+ /** Generated proto lock data */
42
+ this.generatedLockData = null;
43
+ /** Accumulates the Protocol Buffer definition text */
44
+ this.protoText = [];
45
+ /** Current indentation level for formatted output */
46
+ this.indent = 0;
47
+ /** Tracks types that have already been processed to avoid duplication */
48
+ this.processedTypes = new Set();
49
+ /** Queue of types that need to be converted to Proto messages */
50
+ this.messageQueue = [];
51
+ /** Track generated nested list wrapper messages */
52
+ this.nestedListWrappers = new Map();
53
+ /**
54
+ * Map of message names to their field numbers for tracking deleted fields
55
+ * This maintains field numbers even when fields are removed from the schema
56
+ */
57
+ this.fieldNumbersMap = {};
58
+ const { serviceName = 'DefaultService', packageName = 'service.v1', goPackage, lockData, includeComments = true, } = options;
59
+ this.schema = schema;
60
+ this.serviceName = serviceName;
61
+ this.lockManager = new ProtoLockManager(lockData);
62
+ this.includeComments = includeComments;
63
+ // If we have lock data, initialize the field numbers map
64
+ if (lockData) {
65
+ this.initializeFieldNumbersMap(lockData);
66
+ }
67
+ // Generate default go_package if not provided
68
+ const defaultGoPackage = `cosmo/pkg/proto/${packageName};${packageName.replace('.', '')}`;
69
+ const goPackageOption = goPackage || defaultGoPackage;
70
+ // Initialize the Proto definition with the standard header
71
+ this.protoText = [
72
+ 'syntax = "proto3";',
73
+ `package ${packageName};`,
74
+ '',
75
+ `option go_package = "${goPackageOption}";`,
76
+ '',
77
+ ];
78
+ }
79
+ /**
80
+ * Initialize the field numbers map from the lock data to preserve field numbers
81
+ * even when fields are removed and later re-added
82
+ */
83
+ initializeFieldNumbersMap(lockData) {
84
+ this.fieldNumbersMap = {};
85
+ // For each message in the lock data, create a mapping of field name to field number
86
+ for (const [messageName, messageLock] of Object.entries(lockData.messages)) {
87
+ if (!this.fieldNumbersMap[messageName]) {
88
+ this.fieldNumbersMap[messageName] = {};
89
+ }
90
+ // Store field number by field name
91
+ for (const [fieldName, fieldNumber] of Object.entries(messageLock.fields)) {
92
+ this.fieldNumbersMap[messageName][fieldName] = fieldNumber;
93
+ }
94
+ }
95
+ }
96
+ /**
97
+ * Track removed fields for a given message type between schema updates.
98
+ * This ensures that when fields are removed within a single operation,
99
+ * their numbers are properly reserved.
100
+ *
101
+ * @param typeName - The message type name
102
+ * @param originalFieldNames - Original field names from the lock data
103
+ * @param currentFieldNames - Current field names from the schema
104
+ */
105
+ trackRemovedFields(typeName, originalFieldNames, currentFieldNames) {
106
+ var _a;
107
+ // Skip if no lock data exists for this type
108
+ if (!this.lockManager.getLockData().messages[typeName]) {
109
+ return;
110
+ }
111
+ const lockData = this.lockManager.getLockData();
112
+ // Find fields that were in the original type but are no longer present
113
+ const removedFields = originalFieldNames.filter((field) => !currentFieldNames.includes(field));
114
+ if (removedFields.length === 0) {
115
+ return;
116
+ }
117
+ // Get the field numbers for removed fields
118
+ const removedFieldNumbers = [];
119
+ for (const field of removedFields) {
120
+ const fieldNumber = (_a = lockData.messages[typeName]) === null || _a === void 0 ? void 0 : _a.fields[field];
121
+ if (fieldNumber !== undefined) {
122
+ removedFieldNumbers.push(fieldNumber);
123
+ }
124
+ }
125
+ // Add to existing reserved numbers
126
+ if (removedFieldNumbers.length > 0) {
127
+ const existingReserved = lockData.messages[typeName].reservedNumbers || [];
128
+ lockData.messages[typeName].reservedNumbers = [...new Set([...existingReserved, ...removedFieldNumbers])];
129
+ }
130
+ }
131
+ /**
132
+ * Track removed enum values for a given enum type.
133
+ *
134
+ * @param enumName - The enum type name
135
+ * @param originalValueNames - Original enum value names from the lock data
136
+ * @param currentValueNames - Current enum value names from the schema
137
+ */
138
+ trackRemovedEnumValues(enumName, originalValueNames, currentValueNames) {
139
+ var _a;
140
+ // Skip if no lock data exists for this enum
141
+ if (!this.lockManager.getLockData().enums[enumName]) {
142
+ return;
143
+ }
144
+ const lockData = this.lockManager.getLockData();
145
+ // Find values that were in the original enum but are no longer present
146
+ const removedValues = originalValueNames.filter((value) => !currentValueNames.includes(value));
147
+ if (removedValues.length === 0) {
148
+ return;
149
+ }
150
+ // Get the value numbers for removed values
151
+ const removedValueNumbers = [];
152
+ for (const value of removedValues) {
153
+ const valueNumber = (_a = lockData.enums[enumName]) === null || _a === void 0 ? void 0 : _a.fields[value];
154
+ if (valueNumber !== undefined) {
155
+ removedValueNumbers.push(valueNumber);
156
+ }
157
+ }
158
+ // Add to existing reserved numbers
159
+ if (removedValueNumbers.length > 0) {
160
+ const existingReserved = lockData.enums[enumName].reservedNumbers || [];
161
+ lockData.enums[enumName].reservedNumbers = [...new Set([...existingReserved, ...removedValueNumbers])];
162
+ }
163
+ }
164
+ /**
165
+ * Get the proper field number for a field in a message, respecting the lock.
166
+ * This preserves field numbers for fields that were removed and later re-added.
167
+ */
168
+ getFieldNumber(messageName, fieldName, defaultNumber) {
169
+ // Initialize message entry if it doesn't exist
170
+ if (!this.fieldNumbersMap[messageName]) {
171
+ this.fieldNumbersMap[messageName] = {};
172
+ }
173
+ // Direct check if field exists as provided
174
+ if (this.fieldNumbersMap[messageName][fieldName] !== undefined) {
175
+ return this.fieldNumbersMap[messageName][fieldName];
176
+ }
177
+ // Always convert to snake_case for Protocol Buffer field names
178
+ const snakeCaseField = graphqlFieldToProtoField(fieldName);
179
+ // Try the camelCase version for fields that might be stored that way
180
+ const camelCaseField = camelCase(fieldName);
181
+ if (fieldName !== camelCaseField && this.fieldNumbersMap[messageName][camelCaseField] !== undefined) {
182
+ const number = this.fieldNumbersMap[messageName][camelCaseField];
183
+ this.fieldNumbersMap[messageName][fieldName] = number;
184
+ this.fieldNumbersMap[messageName][snakeCaseField] = number; // Also store in snake_case
185
+ return number;
186
+ }
187
+ // Check if this field existed in the lock data
188
+ const lockData = this.lockManager.getLockData();
189
+ if (lockData.messages[messageName]) {
190
+ const fields = lockData.messages[messageName].fields;
191
+ // Check if the field exists in the fields map
192
+ if (fields[fieldName] !== undefined) {
193
+ const fieldNumber = fields[fieldName];
194
+ this.fieldNumbersMap[messageName][fieldName] = fieldNumber;
195
+ if (fieldName !== snakeCaseField) {
196
+ this.fieldNumbersMap[messageName][snakeCaseField] = fieldNumber;
197
+ }
198
+ return fieldNumber;
199
+ }
200
+ // Also check for snake_case version
201
+ if (fieldName !== snakeCaseField && fields[snakeCaseField] !== undefined) {
202
+ const fieldNumber = fields[snakeCaseField];
203
+ this.fieldNumbersMap[messageName][fieldName] = fieldNumber;
204
+ this.fieldNumbersMap[messageName][snakeCaseField] = fieldNumber;
205
+ return fieldNumber;
206
+ }
207
+ }
208
+ // Assign the next available number if the field wasn't found in any previous step
209
+ const nextNumber = this.getNextAvailableFieldNumber(messageName);
210
+ // Store with both the original field name and snake_case for consistency
211
+ this.fieldNumbersMap[messageName][fieldName] = nextNumber;
212
+ if (fieldName !== snakeCaseField) {
213
+ this.fieldNumbersMap[messageName][snakeCaseField] = nextNumber;
214
+ }
215
+ return nextNumber;
216
+ }
217
+ /**
218
+ * Get the next available field number for a message, taking care to avoid
219
+ * collisions with existing field numbers, including for fields that were
220
+ * removed from the schema but may be re-added in the future.
221
+ */
222
+ getNextAvailableFieldNumber(messageName) {
223
+ if (!this.fieldNumbersMap[messageName]) {
224
+ return 1;
225
+ }
226
+ // Get all assigned field numbers for this message
227
+ const usedNumbers = Object.values(this.fieldNumbersMap[messageName]);
228
+ if (usedNumbers.length === 0) {
229
+ return 1;
230
+ }
231
+ // Find the maximum field number and add 1
232
+ return Math.max(...usedNumbers) + 1;
233
+ }
234
+ /**
235
+ * Visit the GraphQL schema to generate Proto buffer definition
236
+ *
237
+ * @returns The complete Protocol Buffer definition as a string
238
+ */
239
+ visit() {
240
+ // Clear the protoText array to just contain the header
241
+ const headerText = this.protoText.slice();
242
+ this.protoText = [];
243
+ // Collect RPC methods and message definitions from all sources
244
+ const entityResult = this.collectEntityRpcMethods();
245
+ const queryResult = this.collectQueryRpcMethods();
246
+ const mutationResult = this.collectMutationRpcMethods();
247
+ // Combine all RPC methods and message definitions
248
+ const allRpcMethods = [...entityResult.rpcMethods, ...queryResult.rpcMethods, ...mutationResult.rpcMethods];
249
+ const allMethodNames = [...entityResult.methodNames, ...queryResult.methodNames, ...mutationResult.methodNames];
250
+ const allMessageDefinitions = [
251
+ ...entityResult.messageDefinitions,
252
+ ...queryResult.messageDefinitions,
253
+ ...mutationResult.messageDefinitions,
254
+ ];
255
+ // Add all types from the schema to the queue that weren't already queued
256
+ this.queueAllSchemaTypes();
257
+ // Start with the header
258
+ this.protoText = headerText;
259
+ // Add a service description comment
260
+ if (this.includeComments) {
261
+ const serviceComment = `Service definition for ${this.serviceName}`;
262
+ this.protoText.push(...this.formatComment(serviceComment, 0)); // Top-level comment, no indent
263
+ }
264
+ // First: Create service block containing only RPC methods
265
+ this.protoText.push(`service ${this.serviceName} {`);
266
+ this.indent++;
267
+ // Sort method names deterministically by alphabetical order
268
+ const orderedMethodNames = [...allMethodNames].sort();
269
+ // Add RPC methods in the ordered sequence
270
+ for (const methodName of orderedMethodNames) {
271
+ const methodIndex = allMethodNames.indexOf(methodName);
272
+ if (methodIndex !== -1) {
273
+ // Handle multi-line RPC definitions that include comments
274
+ const rpcMethodText = allRpcMethods[methodIndex];
275
+ if (rpcMethodText.includes('\n')) {
276
+ // For multi-line RPC method definitions (with comments), add each line separately
277
+ const lines = rpcMethodText.split('\n');
278
+ this.protoText.push(...lines);
279
+ }
280
+ else {
281
+ // For simple one-line RPC method definitions (ensure 2-space indentation)
282
+ this.protoText.push(` ${rpcMethodText}`);
283
+ }
284
+ }
285
+ }
286
+ // Close service definition
287
+ this.indent--;
288
+ this.protoText.push('}');
289
+ this.protoText.push('');
290
+ // Add all wrapper messages first since they might be referenced by other messages
291
+ if (this.nestedListWrappers.size > 0) {
292
+ // Sort the wrappers by name for deterministic output
293
+ const sortedWrapperNames = Array.from(this.nestedListWrappers.keys()).sort();
294
+ for (const wrapperName of sortedWrapperNames) {
295
+ this.protoText.push(this.nestedListWrappers.get(wrapperName));
296
+ }
297
+ }
298
+ // Second: Add all message definitions
299
+ for (const messageDef of allMessageDefinitions) {
300
+ this.protoText.push(messageDef);
301
+ }
302
+ // Third: Process all complex types from the message queue in a single pass
303
+ this.processMessageQueue();
304
+ // Store the generated lock data for retrieval
305
+ this.generatedLockData = this.lockManager.getLockData();
306
+ return this.protoText.join('\n');
307
+ }
308
+ /**
309
+ * Collects RPC methods for entity types (types with @key directive)
310
+ *
311
+ * This method identifies entity types and creates lookup RPC methods
312
+ * for them without generating the request/response messages yet.
313
+ *
314
+ * @returns Object containing RPC methods and message definitions
315
+ */
316
+ collectEntityRpcMethods() {
317
+ var _a, _b, _c, _d;
318
+ const result = { rpcMethods: [], methodNames: [], messageDefinitions: [] };
319
+ const typeMap = this.schema.getTypeMap();
320
+ for (const typeName in typeMap) {
321
+ const type = typeMap[typeName];
322
+ // Skip built-in types and query/mutation/subscription types
323
+ if (typeName.startsWith('__') ||
324
+ typeName === ((_a = this.schema.getQueryType()) === null || _a === void 0 ? void 0 : _a.name) ||
325
+ typeName === ((_b = this.schema.getMutationType()) === null || _b === void 0 ? void 0 : _b.name) ||
326
+ typeName === ((_c = this.schema.getSubscriptionType()) === null || _c === void 0 ? void 0 : _c.name)) {
327
+ continue;
328
+ }
329
+ // Check if this is an entity type (has @key directive)
330
+ if (isObjectType(type)) {
331
+ const astNode = type.astNode;
332
+ const keyDirective = (_d = astNode === null || astNode === void 0 ? void 0 : astNode.directives) === null || _d === void 0 ? void 0 : _d.find((d) => d.name.value === 'key');
333
+ if (keyDirective) {
334
+ // Queue this type for message generation
335
+ this.queueTypeForProcessing(type);
336
+ const keyFields = this.getKeyFieldsFromDirective(keyDirective);
337
+ if (keyFields.length > 0) {
338
+ const keyField = keyFields[0];
339
+ const methodName = createEntityLookupMethodName(typeName, keyField);
340
+ const requestName = createEntityLookupRequestName(typeName, keyField);
341
+ const responseName = createEntityLookupResponseName(typeName, keyField);
342
+ // Add method name and RPC method with description from the entity type
343
+ result.methodNames.push(methodName);
344
+ const description = `Lookup ${typeName} entity by ${keyField}${type.description ? ': ' + type.description : ''}`;
345
+ result.rpcMethods.push(this.createRpcMethod(methodName, requestName, responseName, description));
346
+ // Create request and response messages
347
+ result.messageDefinitions.push(...this.createKeyRequestMessage(typeName, requestName, keyFields[0]));
348
+ result.messageDefinitions.push(...this.createKeyResponseMessage(typeName, responseName));
349
+ }
350
+ }
351
+ }
352
+ }
353
+ return result;
354
+ }
355
+ /**
356
+ * Collects RPC methods for query operations
357
+ *
358
+ * @returns Object containing RPC methods and message definitions
359
+ */
360
+ collectQueryRpcMethods() {
361
+ return this.collectOperationRpcMethods('Query');
362
+ }
363
+ /**
364
+ * Collects RPC methods for mutation operations
365
+ *
366
+ * @returns Object containing RPC methods and message definitions
367
+ */
368
+ collectMutationRpcMethods() {
369
+ return this.collectOperationRpcMethods('Mutation');
370
+ }
371
+ /**
372
+ * Shared method to collect RPC methods for query or mutation operations
373
+ */
374
+ collectOperationRpcMethods(operationType) {
375
+ const result = { rpcMethods: [], methodNames: [], messageDefinitions: [] };
376
+ // Get the root operation type (Query or Mutation)
377
+ const rootType = operationType === 'Query' ? this.schema.getQueryType() : this.schema.getMutationType();
378
+ if (!rootType)
379
+ return result;
380
+ const fields = rootType.getFields();
381
+ // Get field names and order them using the lock manager
382
+ const fieldNames = Object.keys(fields);
383
+ const orderedFieldNames = this.lockManager.reconcileMessageFieldOrder(operationType, fieldNames);
384
+ for (const fieldName of orderedFieldNames) {
385
+ // Skip special fields like _entities
386
+ if (fieldName === '_entities')
387
+ continue;
388
+ if (!fields[fieldName])
389
+ continue;
390
+ const field = fields[fieldName];
391
+ const mappedName = createOperationMethodName(operationType, fieldName);
392
+ const requestName = createRequestMessageName(mappedName);
393
+ const responseName = createResponseMessageName(mappedName);
394
+ // Add method name and RPC method with the field description
395
+ result.methodNames.push(mappedName);
396
+ result.rpcMethods.push(this.createRpcMethod(mappedName, requestName, responseName, field.description));
397
+ // Create request and response messages
398
+ result.messageDefinitions.push(...this.createFieldRequestMessage(requestName, field));
399
+ result.messageDefinitions.push(...this.createFieldResponseMessage(responseName, fieldName, field));
400
+ // Queue the return type for message generation
401
+ this.queueFieldTypeForProcessing(field);
402
+ }
403
+ return result;
404
+ }
405
+ /**
406
+ * Queue a type for processing if not already processed
407
+ */
408
+ queueTypeForProcessing(type) {
409
+ if (!this.processedTypes.has(type.name)) {
410
+ this.messageQueue.push(type);
411
+ }
412
+ }
413
+ /**
414
+ * Queue a field's return type for processing if it's a complex type
415
+ */
416
+ queueFieldTypeForProcessing(field) {
417
+ const returnType = getNamedType(field.type);
418
+ if (!isScalarType(returnType) && !this.processedTypes.has(returnType.name)) {
419
+ this.messageQueue.push(returnType);
420
+ }
421
+ }
422
+ /**
423
+ * Create an RPC method definition with optional comment
424
+ *
425
+ * @param methodName - The name of the RPC method
426
+ * @param requestName - The request message name
427
+ * @param responseName - The response message name
428
+ * @param description - Optional description for the method
429
+ * @returns The RPC method definition with or without comment
430
+ */
431
+ createRpcMethod(methodName, requestName, responseName, description) {
432
+ if (!this.includeComments || !description) {
433
+ return `rpc ${methodName}(${requestName}) returns (${responseName}) {}`;
434
+ }
435
+ // RPC method comments should be indented 1 level (2 spaces)
436
+ const commentLines = this.formatComment(description, 1);
437
+ const methodLine = ` rpc ${methodName}(${requestName}) returns (${responseName}) {}`;
438
+ return [...commentLines, methodLine].join('\n');
439
+ }
440
+ /**
441
+ * Creates a request message for entity lookup without adding to protoText
442
+ */
443
+ createKeyRequestMessage(typeName, requestName, keyField) {
444
+ const messageLines = [];
445
+ const keyMessageName = `${requestName}Key`;
446
+ const lockData = this.lockManager.getLockData();
447
+ // First create the key message
448
+ if (this.includeComments) {
449
+ const keyMessageComment = `Key message for ${typeName} entity lookup`;
450
+ messageLines.push(...this.formatComment(keyMessageComment, 0)); // Top-level comment, no indent
451
+ }
452
+ messageLines.push(`message ${keyMessageName} {`);
453
+ // Add reserved field numbers if any exist for the key message
454
+ const keyMessageLock = lockData.messages[keyMessageName];
455
+ if ((keyMessageLock === null || keyMessageLock === void 0 ? void 0 : keyMessageLock.reservedNumbers) && keyMessageLock.reservedNumbers.length > 0) {
456
+ messageLines.push(` reserved ${this.formatReservedNumbers(keyMessageLock.reservedNumbers)};`);
457
+ }
458
+ // Check for field removals in the key message
459
+ if (lockData.messages[keyMessageName]) {
460
+ const originalKeyFieldNames = Object.keys(lockData.messages[keyMessageName].fields);
461
+ const currentKeyFieldNames = [graphqlFieldToProtoField(keyField)];
462
+ this.trackRemovedFields(keyMessageName, originalKeyFieldNames, currentKeyFieldNames);
463
+ }
464
+ const protoKeyField = graphqlFieldToProtoField(keyField);
465
+ // Get the appropriate field number for the key field
466
+ const keyFieldNumber = this.getFieldNumber(keyMessageName, protoKeyField, 1);
467
+ if (this.includeComments) {
468
+ const keyFieldComment = `Key field for ${typeName} entity lookup`;
469
+ messageLines.push(...this.formatComment(keyFieldComment, 1)); // Field comment, indent 1 level
470
+ }
471
+ messageLines.push(` string ${protoKeyField} = ${keyFieldNumber};`);
472
+ messageLines.push('}');
473
+ messageLines.push('');
474
+ // Ensure the key message is registered in the lock manager data
475
+ this.lockManager.reconcileMessageFieldOrder(keyMessageName, [protoKeyField]);
476
+ // Now create the main request message with a repeated key field
477
+ // Check for field removals in the request message
478
+ if (lockData.messages[requestName]) {
479
+ const originalFieldNames = Object.keys(lockData.messages[requestName].fields);
480
+ const currentFieldNames = ['keys'];
481
+ this.trackRemovedFields(requestName, originalFieldNames, currentFieldNames);
482
+ }
483
+ if (this.includeComments) {
484
+ const requestComment = `Request message for ${typeName} entity lookup`;
485
+ messageLines.push(...this.formatComment(requestComment, 0)); // Top-level comment, no indent
486
+ }
487
+ messageLines.push(`message ${requestName} {`);
488
+ // Add reserved field numbers if any exist for the request message
489
+ const messageLock = lockData.messages[requestName];
490
+ if ((messageLock === null || messageLock === void 0 ? void 0 : messageLock.reservedNumbers) && messageLock.reservedNumbers.length > 0) {
491
+ messageLines.push(` reserved ${this.formatReservedNumbers(messageLock.reservedNumbers)};`);
492
+ }
493
+ // Get the appropriate field number for the repeated key field
494
+ const repeatFieldNumber = this.getFieldNumber(requestName, 'keys', 1);
495
+ if (this.includeComments) {
496
+ const keysComment = `List of keys to look up ${typeName} entities`;
497
+ messageLines.push(...this.formatComment(keysComment, 1)); // Field comment, indent 1 level
498
+ }
499
+ messageLines.push(` repeated ${keyMessageName} keys = ${repeatFieldNumber};`);
500
+ messageLines.push('}');
501
+ messageLines.push('');
502
+ // Ensure the request message is registered in the lock manager data
503
+ this.lockManager.reconcileMessageFieldOrder(requestName, ['keys']);
504
+ return messageLines;
505
+ }
506
+ /**
507
+ * Creates a response message for entity lookup without adding to protoText
508
+ */
509
+ createKeyResponseMessage(typeName, responseName) {
510
+ const messageLines = [];
511
+ const lockData = this.lockManager.getLockData();
512
+ // Check for field removals for the response message
513
+ if (lockData.messages[responseName]) {
514
+ const originalFieldNames = Object.keys(lockData.messages[responseName].fields);
515
+ const currentFieldNames = ['result'];
516
+ this.trackRemovedFields(responseName, originalFieldNames, currentFieldNames);
517
+ }
518
+ // Create the response message with repeated entity directly
519
+ if (this.includeComments) {
520
+ const responseComment = `Response message for ${typeName} entity lookup`;
521
+ messageLines.push(...this.formatComment(responseComment, 0)); // Top-level comment, no indent
522
+ }
523
+ messageLines.push(`message ${responseName} {`);
524
+ // Add reserved field numbers for response message if any exist
525
+ const responseMessageLock = lockData.messages[responseName];
526
+ if ((responseMessageLock === null || responseMessageLock === void 0 ? void 0 : responseMessageLock.reservedNumbers) && responseMessageLock.reservedNumbers.length > 0) {
527
+ messageLines.push(` reserved ${this.formatReservedNumbers(responseMessageLock.reservedNumbers)};`);
528
+ }
529
+ // Get the appropriate field number from the lock
530
+ const responseFieldNumber = this.getFieldNumber(responseName, 'result', 1);
531
+ if (this.includeComments) {
532
+ const resultComment = `List of ${typeName} entities matching the requested keys`;
533
+ messageLines.push(...this.formatComment(resultComment, 1)); // Field comment, indent 1 level
534
+ }
535
+ messageLines.push(` repeated ${typeName} result = ${responseFieldNumber};`);
536
+ messageLines.push('}');
537
+ messageLines.push('');
538
+ // Ensure the response message is registered in the lock manager data
539
+ this.lockManager.reconcileMessageFieldOrder(responseName, ['result']);
540
+ return messageLines;
541
+ }
542
+ /**
543
+ * Creates a request message for a query/mutation field
544
+ */
545
+ createFieldRequestMessage(requestName, field) {
546
+ var _a;
547
+ const messageLines = [];
548
+ // Get current field names and check for removals
549
+ const lockData = this.lockManager.getLockData();
550
+ const argNames = field.args.map((arg) => graphqlFieldToProtoField(arg.name));
551
+ if (lockData.messages[requestName]) {
552
+ const originalFieldNames = Object.keys(lockData.messages[requestName].fields);
553
+ this.trackRemovedFields(requestName, originalFieldNames, argNames);
554
+ }
555
+ // Add a description comment for the request message
556
+ if (this.includeComments) {
557
+ const description = field.description
558
+ ? `Request message for ${field.name} operation${field.description ? ': ' + field.description : ''}`
559
+ : `Request message for ${field.name} operation`;
560
+ messageLines.push(...this.formatComment(description, 0)); // Top-level comment, no indent
561
+ }
562
+ messageLines.push(`message ${requestName} {`);
563
+ // Add reserved field numbers if any exist
564
+ const messageLock = lockData.messages[requestName];
565
+ if ((messageLock === null || messageLock === void 0 ? void 0 : messageLock.reservedNumbers) && messageLock.reservedNumbers.length > 0) {
566
+ messageLines.push(` reserved ${this.formatReservedNumbers(messageLock.reservedNumbers)};`);
567
+ }
568
+ if (field.args.length > 0) {
569
+ const argNames = field.args.map((arg) => arg.name);
570
+ // Extract operation name from the request name (e.g., GetUsersRequest -> GetUsers)
571
+ const operationName = requestName.replace(/Request$/, '');
572
+ // Use the specific argument ordering for this operation
573
+ const orderedArgNames = this.lockManager.reconcileArgumentOrder(operationName, argNames);
574
+ // Process arguments in the order specified by the lock manager
575
+ for (const argName of orderedArgNames) {
576
+ const arg = field.args.find((a) => a.name === argName);
577
+ if (!arg)
578
+ continue;
579
+ const argType = this.getProtoTypeFromGraphQL(arg.type);
580
+ const argProtoName = graphqlFieldToProtoField(arg.name);
581
+ // Get the field number from the messages structure using the original field name
582
+ const fieldNumber = (_a = lockData.messages[operationName]) === null || _a === void 0 ? void 0 : _a.fields[argName];
583
+ // Add argument description as comment
584
+ if (arg.description) {
585
+ // Use 1 level indent for field comments
586
+ messageLines.push(...this.formatComment(arg.description, 1));
587
+ }
588
+ // Check if the argument is a list type and add the repeated keyword if needed
589
+ const isRepeated = isListType(arg.type) || (isNonNullType(arg.type) && isListType(arg.type.ofType));
590
+ if (isRepeated) {
591
+ messageLines.push(` repeated ${argType} ${argProtoName} = ${fieldNumber};`);
592
+ }
593
+ else {
594
+ messageLines.push(` ${argType} ${argProtoName} = ${fieldNumber};`);
595
+ }
596
+ // Add complex input types to the queue for processing
597
+ const namedType = getNamedType(arg.type);
598
+ if (isInputObjectType(namedType) && !this.processedTypes.has(namedType.name)) {
599
+ this.messageQueue.push(namedType);
600
+ }
601
+ }
602
+ }
603
+ messageLines.push('}');
604
+ // Ensure this message is registered in the lock manager data
605
+ if (field.args.length > 0) {
606
+ const fieldNames = field.args.map((arg) => graphqlFieldToProtoField(arg.name));
607
+ this.lockManager.reconcileMessageFieldOrder(requestName, fieldNames);
608
+ }
609
+ return messageLines;
610
+ }
611
+ /**
612
+ * Creates a response message for a query/mutation field
613
+ */
614
+ createFieldResponseMessage(responseName, fieldName, field) {
615
+ const messageLines = [];
616
+ // Check for field removals
617
+ const lockData = this.lockManager.getLockData();
618
+ const protoFieldName = graphqlFieldToProtoField(fieldName);
619
+ if (lockData.messages[responseName]) {
620
+ const originalFieldNames = Object.keys(lockData.messages[responseName].fields);
621
+ this.trackRemovedFields(responseName, originalFieldNames, [protoFieldName]);
622
+ }
623
+ // Add a description comment for the response message
624
+ if (this.includeComments) {
625
+ const description = field.description
626
+ ? `Response message for ${fieldName} operation${field.description ? ': ' + field.description : ''}`
627
+ : `Response message for ${fieldName} operation`;
628
+ messageLines.push(...this.formatComment(description, 0)); // Top-level comment, no indent
629
+ }
630
+ messageLines.push(`message ${responseName} {`);
631
+ // Add reserved field numbers if any exist
632
+ const messageLock = lockData.messages[responseName];
633
+ if ((messageLock === null || messageLock === void 0 ? void 0 : messageLock.reservedNumbers) && messageLock.reservedNumbers.length > 0) {
634
+ messageLines.push(` reserved ${this.formatReservedNumbers(messageLock.reservedNumbers)};`);
635
+ }
636
+ const returnType = this.getProtoTypeFromGraphQL(field.type);
637
+ const isRepeated = isListType(field.type) || (isNonNullType(field.type) && isListType(field.type.ofType));
638
+ // Get the appropriate field number, respecting the lock
639
+ const fieldNumber = this.getFieldNumber(responseName, protoFieldName, 1);
640
+ // Add description for the response field based on field description
641
+ if (field.description) {
642
+ // Use 1 level indent for field comments
643
+ messageLines.push(...this.formatComment(field.description, 1));
644
+ }
645
+ if (isRepeated) {
646
+ messageLines.push(` repeated ${returnType} ${protoFieldName} = ${fieldNumber};`);
647
+ }
648
+ else {
649
+ messageLines.push(` ${returnType} ${protoFieldName} = ${fieldNumber};`);
650
+ }
651
+ messageLines.push('}');
652
+ // Ensure this message is registered in the lock manager data
653
+ this.lockManager.reconcileMessageFieldOrder(responseName, [protoFieldName]);
654
+ return messageLines;
655
+ }
656
+ /**
657
+ * Extract key fields from a directive
658
+ *
659
+ * The @key directive specifies which fields form the entity's primary key.
660
+ * We extract these for creating appropriate lookup methods.
661
+ *
662
+ * @param directive - The @key directive from the GraphQL AST
663
+ * @returns Array of field names that form the key
664
+ */
665
+ getKeyFieldsFromDirective(directive) {
666
+ var _a;
667
+ const fieldsArg = (_a = directive.arguments) === null || _a === void 0 ? void 0 : _a.find((arg) => arg.name.value === 'fields');
668
+ if (fieldsArg && fieldsArg.value.kind === 'StringValue') {
669
+ const stringValue = fieldsArg.value;
670
+ return stringValue.value.split(' ');
671
+ }
672
+ return [];
673
+ }
674
+ /**
675
+ * Queue all types from the schema that need processing
676
+ */
677
+ queueAllSchemaTypes() {
678
+ const typeMap = this.schema.getTypeMap();
679
+ for (const typeName in typeMap) {
680
+ const type = typeMap[typeName];
681
+ // Skip built-in types, Query type, _Entity, and already processed types
682
+ if (typeName.startsWith('__') ||
683
+ typeName === 'Query' ||
684
+ typeName === 'Mutation' ||
685
+ typeName === '_Entity' ||
686
+ this.processedTypes.has(typeName)) {
687
+ continue;
688
+ }
689
+ // Queue type for processing if it's a complex type
690
+ if (isObjectType(type) ||
691
+ isInputObjectType(type) ||
692
+ isInterfaceType(type) ||
693
+ isUnionType(type) ||
694
+ isEnumType(type)) {
695
+ this.messageQueue.push(type);
696
+ }
697
+ }
698
+ }
699
+ /**
700
+ * Process all queued complex types for message generation
701
+ *
702
+ * This is a key method that processes the message queue to generate
703
+ * Protocol Buffer messages for all complex types. The queue approach ensures:
704
+ *
705
+ * 1. All referenced types are eventually processed
706
+ * 2. Types are only processed once (avoids duplication)
707
+ * 3. Circular references are handled properly
708
+ * 4. Dependencies between types are resolved correctly
709
+ */
710
+ processMessageQueue() {
711
+ // Process queued types in a single pass
712
+ while (this.messageQueue.length > 0) {
713
+ const type = this.messageQueue.shift();
714
+ // Skip already processed types and special internal types
715
+ if (this.processedTypes.has(type.name) || type.name === '_Entity') {
716
+ continue;
717
+ }
718
+ // Process the type based on its kind
719
+ if (isObjectType(type)) {
720
+ this.processObjectType(type);
721
+ }
722
+ else if (isInputObjectType(type)) {
723
+ this.processInputObjectType(type);
724
+ }
725
+ else if (isInterfaceType(type)) {
726
+ this.processInterfaceType(type);
727
+ }
728
+ else if (isUnionType(type)) {
729
+ this.processUnionType(type);
730
+ }
731
+ else if (isEnumType(type)) {
732
+ this.processEnumType(type);
733
+ }
734
+ // Mark as processed
735
+ this.processedTypes.add(type.name);
736
+ }
737
+ }
738
+ /**
739
+ * Process a GraphQL object type to a Proto message
740
+ *
741
+ * Converts a GraphQL object type to a Protocol Buffer message with
742
+ * fields corresponding to the GraphQL object fields.
743
+ *
744
+ * @param type - The GraphQL object type
745
+ */
746
+ processObjectType(type) {
747
+ // Skip creating a message for special entity type
748
+ if (type.name === '_Entity') {
749
+ this.processedTypes.add(type.name);
750
+ return;
751
+ }
752
+ // Check for field removals if lock data exists for this type
753
+ const lockData = this.lockManager.getLockData();
754
+ if (lockData.messages[type.name]) {
755
+ const originalFieldNames = Object.keys(lockData.messages[type.name].fields);
756
+ const currentFieldNames = Object.keys(type.getFields());
757
+ this.trackRemovedFields(type.name, originalFieldNames, currentFieldNames);
758
+ }
759
+ this.protoText.push('');
760
+ // Add type description as comment before message definition
761
+ if (type.description) {
762
+ this.protoText.push(...this.formatComment(type.description, 0)); // Top-level comment, no indent
763
+ }
764
+ this.protoText.push(`message ${type.name} {`);
765
+ this.indent++;
766
+ // Add reserved field numbers if any exist
767
+ const messageLock = lockData.messages[type.name];
768
+ if ((messageLock === null || messageLock === void 0 ? void 0 : messageLock.reservedNumbers) && messageLock.reservedNumbers.length > 0) {
769
+ this.protoText.push(` reserved ${this.formatReservedNumbers(messageLock.reservedNumbers)};`);
770
+ }
771
+ const fields = type.getFields();
772
+ // Get field names and order them using the lock manager
773
+ const fieldNames = Object.keys(fields);
774
+ const orderedFieldNames = this.lockManager.reconcileMessageFieldOrder(type.name, fieldNames);
775
+ for (const fieldName of orderedFieldNames) {
776
+ if (!fields[fieldName])
777
+ continue;
778
+ const field = fields[fieldName];
779
+ const fieldType = this.getProtoTypeFromGraphQL(field.type);
780
+ const isRepeated = isListType(field.type) || (isNonNullType(field.type) && isListType(field.type.ofType));
781
+ const protoFieldName = graphqlFieldToProtoField(fieldName);
782
+ // Get the appropriate field number, respecting the lock
783
+ const fieldNumber = this.getFieldNumber(type.name, protoFieldName, this.getNextAvailableFieldNumber(type.name));
784
+ // Add field description as comment
785
+ if (field.description) {
786
+ this.protoText.push(...this.formatComment(field.description, 1)); // Field comment, indent 1 level
787
+ }
788
+ if (isRepeated) {
789
+ this.protoText.push(` repeated ${fieldType} ${protoFieldName} = ${fieldNumber};`);
790
+ }
791
+ else {
792
+ this.protoText.push(` ${fieldType} ${protoFieldName} = ${fieldNumber};`);
793
+ }
794
+ // Queue complex field types for processing
795
+ const namedType = getNamedType(field.type);
796
+ if (!isScalarType(namedType) && !this.processedTypes.has(namedType.name)) {
797
+ this.messageQueue.push(namedType);
798
+ }
799
+ }
800
+ this.indent--;
801
+ this.protoText.push('}');
802
+ }
803
+ /**
804
+ * Process a GraphQL input object type to a Proto message
805
+ *
806
+ * Converts a GraphQL input object type to a Protocol Buffer message
807
+ * with fields corresponding to the GraphQL input object fields.
808
+ *
809
+ * @param type - The GraphQL input object type
810
+ */
811
+ processInputObjectType(type) {
812
+ // Check for field removals if lock data exists for this type
813
+ const lockData = this.lockManager.getLockData();
814
+ if (lockData.messages[type.name]) {
815
+ const originalFieldNames = Object.keys(lockData.messages[type.name].fields);
816
+ const currentFieldNames = Object.keys(type.getFields());
817
+ this.trackRemovedFields(type.name, originalFieldNames, currentFieldNames);
818
+ }
819
+ this.protoText.push('');
820
+ // Add type description as comment before message definition
821
+ if (type.description) {
822
+ this.protoText.push(...this.formatComment(type.description, 0)); // Top-level comment, no indent
823
+ }
824
+ this.protoText.push(`message ${type.name} {`);
825
+ this.indent++;
826
+ // Add reserved field numbers if any exist
827
+ const messageLock = lockData.messages[type.name];
828
+ if ((messageLock === null || messageLock === void 0 ? void 0 : messageLock.reservedNumbers) && messageLock.reservedNumbers.length > 0) {
829
+ this.protoText.push(` reserved ${this.formatReservedNumbers(messageLock.reservedNumbers)};`);
830
+ }
831
+ const fields = type.getFields();
832
+ // Get field names and order them using the lock manager
833
+ const fieldNames = Object.keys(fields);
834
+ const orderedFieldNames = this.lockManager.reconcileMessageFieldOrder(type.name, fieldNames);
835
+ for (const fieldName of orderedFieldNames) {
836
+ if (!fields[fieldName])
837
+ continue;
838
+ const field = fields[fieldName];
839
+ const fieldType = this.getProtoTypeFromGraphQL(field.type);
840
+ const isRepeated = isListType(field.type) || (isNonNullType(field.type) && isListType(field.type.ofType));
841
+ const protoFieldName = graphqlFieldToProtoField(fieldName);
842
+ // Get the appropriate field number, respecting the lock
843
+ const fieldNumber = this.getFieldNumber(type.name, protoFieldName, this.getNextAvailableFieldNumber(type.name));
844
+ // Add field description as comment
845
+ if (field.description) {
846
+ this.protoText.push(...this.formatComment(field.description, 1)); // Field comment, indent 1 level
847
+ }
848
+ if (isRepeated) {
849
+ this.protoText.push(` repeated ${fieldType} ${protoFieldName} = ${fieldNumber};`);
850
+ }
851
+ else {
852
+ this.protoText.push(` ${fieldType} ${protoFieldName} = ${fieldNumber};`);
853
+ }
854
+ // Queue complex field types for processing
855
+ const namedType = getNamedType(field.type);
856
+ if (!isScalarType(namedType) && !this.processedTypes.has(namedType.name)) {
857
+ this.messageQueue.push(namedType);
858
+ }
859
+ }
860
+ this.indent--;
861
+ this.protoText.push('}');
862
+ }
863
+ /**
864
+ * Process a GraphQL interface type
865
+ *
866
+ * In Protocol Buffers, we handle interfaces using the 'oneof' feature
867
+ * with all implementing types as options. This allows for polymorphic
868
+ * behavior similar to GraphQL interfaces.
869
+ *
870
+ * @param type - The GraphQL interface type
871
+ */
872
+ processInterfaceType(type) {
873
+ // Mark the interface as processed to avoid infinite recursion
874
+ this.processedTypes.add(type.name);
875
+ const implementingTypes = Object.values(this.schema.getTypeMap())
876
+ .filter(isObjectType)
877
+ .filter((t) => t.getInterfaces().some((i) => i.name === type.name));
878
+ if (implementingTypes.length === 0) {
879
+ // No implementing types, just create a regular message
880
+ this.processObjectType(type);
881
+ return;
882
+ }
883
+ this.protoText.push('');
884
+ // Add interface description as comment
885
+ if (type.description) {
886
+ this.protoText.push(...this.formatComment(type.description, 0)); // Top-level comment, no indent
887
+ }
888
+ this.protoText.push(`message ${type.name} {`);
889
+ this.indent++;
890
+ // Create a oneof field with all implementing types
891
+ this.protoText.push(` oneof instance {`);
892
+ this.indent++;
893
+ // Use lock manager to order implementing types
894
+ const typeNames = implementingTypes.map((t) => t.name);
895
+ const orderedTypeNames = this.lockManager.reconcileMessageFieldOrder(`${type.name}Implementations`, typeNames);
896
+ for (let i = 0; i < orderedTypeNames.length; i++) {
897
+ const typeName = orderedTypeNames[i];
898
+ const implType = implementingTypes.find((t) => t.name === typeName);
899
+ if (!implType)
900
+ continue;
901
+ // Add implementing type description as comment if available
902
+ if (implType.description) {
903
+ this.protoText.push(...this.formatComment(implType.description, 1)); // Field comment, indent 1 level
904
+ }
905
+ this.protoText.push(` ${implType.name} ${graphqlFieldToProtoField(implType.name)} = ${i + 1};`);
906
+ // Queue implementing types for processing
907
+ if (!this.processedTypes.has(implType.name)) {
908
+ this.messageQueue.push(implType);
909
+ }
910
+ }
911
+ this.indent--;
912
+ this.protoText.push(` }`);
913
+ this.indent--;
914
+ this.protoText.push('}');
915
+ }
916
+ /**
917
+ * Process a GraphQL union type
918
+ *
919
+ * Similar to interfaces, we handle GraphQL unions using Protocol Buffer's
920
+ * 'oneof' feature with all member types as options.
921
+ *
922
+ * @param type - The GraphQL union type
923
+ */
924
+ processUnionType(type) {
925
+ // Skip processing _Entity union type
926
+ if (type.name === '_Entity') {
927
+ this.processedTypes.add(type.name);
928
+ return;
929
+ }
930
+ this.protoText.push('');
931
+ // Add union description as comment
932
+ if (type.description) {
933
+ this.protoText.push(...this.formatComment(type.description, 0)); // Top-level comment, no indent
934
+ }
935
+ this.protoText.push(`message ${type.name} {`);
936
+ this.indent++;
937
+ // Create a oneof field with all member types
938
+ this.protoText.push(` oneof value {`);
939
+ this.indent++;
940
+ // Use lock manager to order union member types
941
+ const types = type.getTypes();
942
+ const typeNames = types.map((t) => t.name);
943
+ const orderedTypeNames = this.lockManager.reconcileMessageFieldOrder(`${type.name}Members`, typeNames);
944
+ for (let i = 0; i < orderedTypeNames.length; i++) {
945
+ const typeName = orderedTypeNames[i];
946
+ const memberType = types.find((t) => t.name === typeName);
947
+ if (!memberType)
948
+ continue;
949
+ // Add member type description as comment if available
950
+ if (memberType.description) {
951
+ this.protoText.push(...this.formatComment(memberType.description, 1)); // Field comment, indent 1 level
952
+ }
953
+ this.protoText.push(` ${memberType.name} ${graphqlFieldToProtoField(memberType.name)} = ${i + 1};`);
954
+ // Queue member types for processing
955
+ if (!this.processedTypes.has(memberType.name)) {
956
+ this.messageQueue.push(memberType);
957
+ }
958
+ }
959
+ this.indent--;
960
+ this.protoText.push(` }`);
961
+ this.indent--;
962
+ this.protoText.push('}');
963
+ }
964
+ /**
965
+ * Process a GraphQL enum type to a Proto enum
966
+ *
967
+ * Converts a GraphQL enum to a Protocol Buffer enum. Note that Proto3
968
+ * requires the first enum value to be zero, so we add an UNSPECIFIED value.
969
+ *
970
+ * @param type - The GraphQL enum type
971
+ */
972
+ processEnumType(type) {
973
+ // Check for enum value removals if lock data exists for this enum
974
+ const lockData = this.lockManager.getLockData();
975
+ if (lockData.enums[type.name]) {
976
+ const originalValueNames = Object.keys(lockData.enums[type.name].fields);
977
+ const currentValueNames = type.getValues().map((v) => v.name);
978
+ this.trackRemovedEnumValues(type.name, originalValueNames, currentValueNames);
979
+ }
980
+ this.protoText.push('');
981
+ // Add enum description as comment
982
+ if (type.description) {
983
+ this.protoText.push(...this.formatComment(type.description, 0)); // Top-level comment, no indent
984
+ }
985
+ this.protoText.push(`enum ${type.name} {`);
986
+ this.indent++;
987
+ // Add reserved enum values first if any exist
988
+ const enumLock = lockData.enums[type.name];
989
+ if ((enumLock === null || enumLock === void 0 ? void 0 : enumLock.reservedNumbers) && enumLock.reservedNumbers.length > 0) {
990
+ this.protoText.push(` reserved ${this.formatReservedNumbers(enumLock.reservedNumbers)};`);
991
+ }
992
+ // Add unspecified value as first enum value (required in proto3)
993
+ const unspecifiedValue = createEnumUnspecifiedValue(type.name);
994
+ this.protoText.push(` ${unspecifiedValue} = 0;`);
995
+ // Use lock manager to order enum values
996
+ const values = type.getValues();
997
+ const valueNames = values.map((v) => v.name);
998
+ const orderedValueNames = this.lockManager.reconcileEnumValueOrder(type.name, valueNames);
999
+ for (const valueName of orderedValueNames) {
1000
+ const value = values.find((v) => v.name === valueName);
1001
+ if (!value)
1002
+ continue;
1003
+ const protoEnumValue = graphqlEnumValueToProtoEnumValue(type.name, value.name);
1004
+ // Add enum value description as comment
1005
+ if (value.description) {
1006
+ this.protoText.push(...this.formatComment(value.description, 1)); // Field comment, indent 1 level
1007
+ }
1008
+ // Get value number from lock data
1009
+ const lockData = this.lockManager.getLockData();
1010
+ let valueNumber = 0;
1011
+ if (lockData.enums[type.name] && lockData.enums[type.name].fields[value.name]) {
1012
+ valueNumber = lockData.enums[type.name].fields[value.name];
1013
+ }
1014
+ else {
1015
+ // This should never happen since we just reconciled, but just in case
1016
+ console.warn(`Missing enum value number for ${type.name}.${value.name}`);
1017
+ continue;
1018
+ }
1019
+ this.protoText.push(` ${protoEnumValue} = ${valueNumber};`);
1020
+ }
1021
+ this.indent--;
1022
+ this.protoText.push('}');
1023
+ }
1024
+ /**
1025
+ * Map GraphQL type to Protocol Buffer type
1026
+ *
1027
+ * Determines the appropriate Protocol Buffer type for a given GraphQL type,
1028
+ * handling all GraphQL type wrappers (NonNull, List) correctly.
1029
+ *
1030
+ * @param graphqlType - The GraphQL type to convert
1031
+ * @returns The corresponding Protocol Buffer type name
1032
+ */
1033
+ getProtoTypeFromGraphQL(graphqlType) {
1034
+ if (isScalarType(graphqlType)) {
1035
+ return SCALAR_TYPE_MAP[graphqlType.name] || 'string';
1036
+ }
1037
+ if (isEnumType(graphqlType)) {
1038
+ return graphqlType.name;
1039
+ }
1040
+ if (isNonNullType(graphqlType)) {
1041
+ return this.getProtoTypeFromGraphQL(graphqlType.ofType);
1042
+ }
1043
+ if (isListType(graphqlType)) {
1044
+ // Handle nested list types (e.g., [[Type]])
1045
+ const innerType = graphqlType.ofType;
1046
+ // If the inner type is also a list, we need to use a wrapper message
1047
+ if (isListType(innerType) || (isNonNullType(innerType) && isListType(innerType.ofType))) {
1048
+ // Find the most inner type by unwrapping all lists and non-nulls
1049
+ let currentType = innerType;
1050
+ while (isListType(currentType) || isNonNullType(currentType)) {
1051
+ currentType = isListType(currentType) ? currentType.ofType : currentType.ofType;
1052
+ }
1053
+ // Get the name of the inner type and create wrapper name
1054
+ const namedInnerType = currentType;
1055
+ const wrapperName = `${namedInnerType.name}List`;
1056
+ // Generate the wrapper message if not already created
1057
+ if (!this.processedTypes.has(wrapperName) && !this.nestedListWrappers.has(wrapperName)) {
1058
+ this.createNestedListWrapper(wrapperName, namedInnerType);
1059
+ }
1060
+ return wrapperName;
1061
+ }
1062
+ return this.getProtoTypeFromGraphQL(innerType);
1063
+ }
1064
+ // Named types (object, interface, union, input)
1065
+ const namedType = graphqlType;
1066
+ if (namedType && typeof namedType.name === 'string') {
1067
+ return namedType.name;
1068
+ }
1069
+ return 'string'; // Default fallback
1070
+ }
1071
+ /**
1072
+ * Create a nested list wrapper message for the given base type
1073
+ */
1074
+ createNestedListWrapper(wrapperName, baseType) {
1075
+ // Skip if already processed
1076
+ if (this.processedTypes.has(wrapperName) || this.nestedListWrappers.has(wrapperName)) {
1077
+ return;
1078
+ }
1079
+ // Mark as processed to avoid recursion
1080
+ this.processedTypes.add(wrapperName);
1081
+ // Check for field removals if lock data exists for this wrapper
1082
+ const lockData = this.lockManager.getLockData();
1083
+ if (lockData.messages[wrapperName]) {
1084
+ const originalFieldNames = Object.keys(lockData.messages[wrapperName].fields);
1085
+ const currentFieldNames = ['result'];
1086
+ this.trackRemovedFields(wrapperName, originalFieldNames, currentFieldNames);
1087
+ }
1088
+ // Create a temporary array for the wrapper definition
1089
+ const messageLines = [];
1090
+ // Add a description comment for the wrapper message
1091
+ if (this.includeComments) {
1092
+ const wrapperComment = `Wrapper message for a list of ${baseType.name}`;
1093
+ messageLines.push(...this.formatComment(wrapperComment, 0)); // Top-level comment, no indent
1094
+ }
1095
+ messageLines.push(`message ${wrapperName} {`);
1096
+ // Add reserved field numbers if any exist
1097
+ const messageLock = lockData.messages[wrapperName];
1098
+ if ((messageLock === null || messageLock === void 0 ? void 0 : messageLock.reservedNumbers) && messageLock.reservedNumbers.length > 0) {
1099
+ messageLines.push(` reserved ${this.formatReservedNumbers(messageLock.reservedNumbers)};`);
1100
+ }
1101
+ // Get the appropriate field number from the lock
1102
+ const fieldNumber = this.getFieldNumber(wrapperName, 'result', 1);
1103
+ // For the inner type, we need to get the proto type for the base type
1104
+ const protoType = this.getProtoTypeFromGraphQL(baseType);
1105
+ messageLines.push(` repeated ${protoType} result = ${fieldNumber};`);
1106
+ messageLines.push('}');
1107
+ messageLines.push('');
1108
+ // Ensure the wrapper message is registered in the lock manager data
1109
+ this.lockManager.reconcileMessageFieldOrder(wrapperName, ['result']);
1110
+ // Store the wrapper message for later inclusion in the output
1111
+ this.nestedListWrappers.set(wrapperName, messageLines.join('\n'));
1112
+ }
1113
+ /**
1114
+ * Get indentation based on the current level
1115
+ *
1116
+ * Helper method to maintain consistent indentation in the output.
1117
+ *
1118
+ * @returns String with spaces for the current indentation level
1119
+ */
1120
+ getIndent() {
1121
+ return ' '.repeat(this.indent);
1122
+ }
1123
+ /**
1124
+ * Get the generated lock data after visiting
1125
+ *
1126
+ * @returns The generated ProtoLock data, or null if visit() hasn't been called
1127
+ */
1128
+ getGeneratedLockData() {
1129
+ return this.generatedLockData;
1130
+ }
1131
+ /**
1132
+ * Format reserved numbers for Proto syntax
1133
+ *
1134
+ * Formats a list of reserved field numbers for inclusion in a Proto message.
1135
+ * This handles both individual numbers and ranges.
1136
+ *
1137
+ * @param numbers - The field numbers to be reserved
1138
+ * @returns A formatted string for the reserved statement
1139
+ */
1140
+ formatReservedNumbers(numbers) {
1141
+ if (numbers.length === 0)
1142
+ return '';
1143
+ // Sort numbers for better readability
1144
+ const sortedNumbers = [...numbers].sort((a, b) => a - b);
1145
+ // Simple case: only one number
1146
+ if (sortedNumbers.length === 1) {
1147
+ return sortedNumbers[0].toString();
1148
+ }
1149
+ // Find continuous ranges to compact the representation
1150
+ const ranges = [];
1151
+ let rangeStart = sortedNumbers[0];
1152
+ let rangeEnd = sortedNumbers[0];
1153
+ for (let i = 1; i < sortedNumbers.length; i++) {
1154
+ if (sortedNumbers[i] === rangeEnd + 1) {
1155
+ // Extend the current range
1156
+ rangeEnd = sortedNumbers[i];
1157
+ }
1158
+ else {
1159
+ // End the current range and start a new one
1160
+ ranges.push([rangeStart, rangeEnd]);
1161
+ rangeStart = sortedNumbers[i];
1162
+ rangeEnd = sortedNumbers[i];
1163
+ }
1164
+ }
1165
+ // Add the last range
1166
+ ranges.push([rangeStart, rangeEnd]);
1167
+ // Format the ranges
1168
+ return ranges
1169
+ .map(([start, end]) => {
1170
+ if (start === end) {
1171
+ return start.toString();
1172
+ }
1173
+ else {
1174
+ return `${start} to ${end}`;
1175
+ }
1176
+ })
1177
+ .join(', ');
1178
+ }
1179
+ /**
1180
+ * Convert a GraphQL description to Protocol Buffer comment
1181
+ * @param description - The GraphQL description text
1182
+ * @param indentLevel - The level of indentation for the comment (in number of 2-space blocks)
1183
+ * @returns Array of comment lines with proper indentation
1184
+ */
1185
+ formatComment(description, indentLevel = 0) {
1186
+ if (!this.includeComments || !description) {
1187
+ return [];
1188
+ }
1189
+ // Use 2-space indentation consistently
1190
+ const indent = ' '.repeat(indentLevel);
1191
+ const lines = description.trim().split('\n');
1192
+ if (lines.length === 1) {
1193
+ return [`${indent}// ${lines[0]}`];
1194
+ }
1195
+ else {
1196
+ return [`${indent}/*`, ...lines.map((line) => `${indent} * ${line}`), `${indent} */`];
1197
+ }
1198
+ }
1199
+ }
1200
+ //# sourceMappingURL=sdl-to-proto-visitor.js.map