@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.
- package/LICENSE +201 -0
- package/README.md +162 -0
- package/dist/src/index.d.ts +37 -0
- package/dist/src/index.js +53 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/naming-conventions.d.ts +50 -0
- package/dist/src/naming-conventions.js +62 -0
- package/dist/src/naming-conventions.js.map +1 -0
- package/dist/src/proto-lock.d.ts +66 -0
- package/dist/src/proto-lock.js +150 -0
- package/dist/src/proto-lock.js.map +1 -0
- package/dist/src/sdl-to-mapping-visitor.d.ts +172 -0
- package/dist/src/sdl-to-mapping-visitor.js +365 -0
- package/dist/src/sdl-to-mapping-visitor.js.map +1 -0
- package/dist/src/sdl-to-proto-visitor.d.ts +278 -0
- package/dist/src/sdl-to-proto-visitor.js +1200 -0
- package/dist/src/sdl-to-proto-visitor.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +45 -0
|
@@ -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
|