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