@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,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Class to manage proto lock data for deterministic field ordering
|
|
3
|
+
*/
|
|
4
|
+
export class ProtoLockManager {
|
|
5
|
+
/**
|
|
6
|
+
* Create a new ProtoLockManager
|
|
7
|
+
*
|
|
8
|
+
* @param initialLockData - Initial lock data to use, if any
|
|
9
|
+
*/
|
|
10
|
+
constructor(initialLockData) {
|
|
11
|
+
this.lockData = initialLockData || {
|
|
12
|
+
version: '1.0.0',
|
|
13
|
+
messages: {},
|
|
14
|
+
enums: {},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Generic method to reconcile items and their numbers
|
|
19
|
+
*
|
|
20
|
+
* @param container - The container object (messages or enums)
|
|
21
|
+
* @param itemName - Name of the item (message or enum)
|
|
22
|
+
* @param availableItems - Available item names (fields or enum values)
|
|
23
|
+
* @returns Ordered array of item names
|
|
24
|
+
*/
|
|
25
|
+
reconcileItems(container, itemName, availableItems) {
|
|
26
|
+
// Ensure container item exists
|
|
27
|
+
if (!container[itemName]) {
|
|
28
|
+
container[itemName] = { fields: {} };
|
|
29
|
+
}
|
|
30
|
+
// Get existing fields map
|
|
31
|
+
const fieldsMap = container[itemName].fields;
|
|
32
|
+
// Get existing reserved numbers
|
|
33
|
+
let reservedNumbers = container[itemName].reservedNumbers || [];
|
|
34
|
+
// Track removed items and their numbers
|
|
35
|
+
const removedItems = {};
|
|
36
|
+
// Identify removed items
|
|
37
|
+
Object.entries(fieldsMap).forEach(([item, number]) => {
|
|
38
|
+
if (!availableItems.includes(item)) {
|
|
39
|
+
reservedNumbers.push(number);
|
|
40
|
+
removedItems[item] = number;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
// Deduplicate reserved numbers
|
|
44
|
+
reservedNumbers = [...new Set(reservedNumbers)];
|
|
45
|
+
// Create new fields map
|
|
46
|
+
const newFieldsMap = {};
|
|
47
|
+
// Preserve existing numbers for items that are still available
|
|
48
|
+
availableItems.forEach((item) => {
|
|
49
|
+
const existingNumber = fieldsMap[item];
|
|
50
|
+
if (existingNumber !== undefined) {
|
|
51
|
+
newFieldsMap[item] = existingNumber;
|
|
52
|
+
// Remove from reserved if it's reused
|
|
53
|
+
const index = reservedNumbers.indexOf(existingNumber);
|
|
54
|
+
if (index !== -1) {
|
|
55
|
+
reservedNumbers.splice(index, 1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
// Get highest assigned number
|
|
60
|
+
let maxNumber = 0;
|
|
61
|
+
Object.values(newFieldsMap).forEach((num) => {
|
|
62
|
+
maxNumber = Math.max(maxNumber, num);
|
|
63
|
+
});
|
|
64
|
+
// Also consider reserved numbers for max
|
|
65
|
+
if (reservedNumbers.length > 0) {
|
|
66
|
+
maxNumber = Math.max(maxNumber, ...reservedNumbers);
|
|
67
|
+
}
|
|
68
|
+
// Assign numbers to items that don't have one
|
|
69
|
+
availableItems.forEach((item) => {
|
|
70
|
+
if (newFieldsMap[item] === undefined) {
|
|
71
|
+
// Check if the item was previously removed (exists in our reservedNumbers)
|
|
72
|
+
let reservedNumber;
|
|
73
|
+
Object.entries(removedItems).forEach(([removedItem, number]) => {
|
|
74
|
+
if (removedItem === item && reservedNumbers.includes(number)) {
|
|
75
|
+
reservedNumber = number;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
if (reservedNumber !== undefined) {
|
|
79
|
+
// Reuse the reserved number for this item
|
|
80
|
+
newFieldsMap[item] = reservedNumber;
|
|
81
|
+
// Remove from reserved list
|
|
82
|
+
const index = reservedNumbers.indexOf(reservedNumber);
|
|
83
|
+
if (index !== -1) {
|
|
84
|
+
reservedNumbers.splice(index, 1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// Find next available number
|
|
89
|
+
let nextNumber = maxNumber + 1;
|
|
90
|
+
while (reservedNumbers.includes(nextNumber)) {
|
|
91
|
+
nextNumber++;
|
|
92
|
+
}
|
|
93
|
+
newFieldsMap[item] = nextNumber;
|
|
94
|
+
maxNumber = nextNumber;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
// Update the fields map and reserved numbers
|
|
99
|
+
container[itemName].fields = newFieldsMap;
|
|
100
|
+
if (reservedNumbers.length > 0) {
|
|
101
|
+
container[itemName].reservedNumbers = reservedNumbers;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// If no reserved numbers, make sure the property doesn't exist
|
|
105
|
+
delete container[itemName].reservedNumbers;
|
|
106
|
+
}
|
|
107
|
+
// Sort available items by their assigned numbers
|
|
108
|
+
return [...availableItems].sort((a, b) => {
|
|
109
|
+
return newFieldsMap[a] - newFieldsMap[b];
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Reconcile and get the ordered field names for a message
|
|
114
|
+
*
|
|
115
|
+
* @param messageName - Name of the message
|
|
116
|
+
* @param availableFields - Available field names (used when no lock exists)
|
|
117
|
+
* @returns Ordered array of field names
|
|
118
|
+
*/
|
|
119
|
+
reconcileMessageFieldOrder(messageName, availableFields) {
|
|
120
|
+
return this.reconcileItems(this.lockData.messages, messageName, availableFields);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Reconcile and get the ordered enum values
|
|
124
|
+
*
|
|
125
|
+
* @param enumName - Name of the enum
|
|
126
|
+
* @param availableValues - Available enum values (used when no lock exists)
|
|
127
|
+
* @returns Ordered array of enum values
|
|
128
|
+
*/
|
|
129
|
+
reconcileEnumValueOrder(enumName, availableValues) {
|
|
130
|
+
return this.reconcileItems(this.lockData.enums, enumName, availableValues);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Reconcile and get the ordered argument names for a field
|
|
134
|
+
*
|
|
135
|
+
* @param fieldPath - Path to the field in the format "TypeName.fieldName"
|
|
136
|
+
* @param availableArgs - Available argument names (used when no lock exists)
|
|
137
|
+
* @returns Ordered array of argument names
|
|
138
|
+
*/
|
|
139
|
+
reconcileArgumentOrder(fieldPath, availableArgs) {
|
|
140
|
+
// Use the regular message field ordering for arguments
|
|
141
|
+
return this.reconcileMessageFieldOrder(fieldPath, availableArgs);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Get the current lock data
|
|
145
|
+
*/
|
|
146
|
+
getLockData() {
|
|
147
|
+
return this.lockData;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=proto-lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proto-lock.js","sourceRoot":"","sources":["../../src/proto-lock.ts"],"names":[],"mappings":"AAmBA;;GAEG;AACH,MAAM,OAAO,gBAAgB;IAG3B;;;;OAIG;IACH,YAAY,eAA2B;QACrC,IAAI,CAAC,QAAQ,GAAG,eAAe,IAAI;YACjC,OAAO,EAAE,OAAO;YAChB,QAAQ,EAAE,EAAE;YACZ,KAAK,EAAE,EAAE;SACV,CAAC;IACJ,CAAC;IAED;;;;;;;OAOG;IACK,cAAc,CACpB,SAA4B,EAC5B,QAAgB,EAChB,cAAwB;QAExB,+BAA+B;QAC/B,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC;YACzB,SAAS,CAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAO,CAAC;QAC5C,CAAC;QAED,0BAA0B;QAC1B,MAAM,SAAS,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC;QAE7C,gCAAgC;QAChC,IAAI,eAAe,GAAa,SAAS,CAAC,QAAQ,CAAC,CAAC,eAAe,IAAI,EAAE,CAAC;QAE1E,wCAAwC;QACxC,MAAM,YAAY,GAA2B,EAAE,CAAC;QAEhD,yBAAyB;QACzB,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE;YACnD,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;gBACnC,eAAe,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;gBAC7B,YAAY,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC;YAC9B,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,+BAA+B;QAC/B,eAAe,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC;QAEhD,wBAAwB;QACxB,MAAM,YAAY,GAA2B,EAAE,CAAC;QAEhD,+DAA+D;QAC/D,cAAc,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;YAC9B,MAAM,cAAc,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;YACvC,IAAI,cAAc,KAAK,SAAS,EAAE,CAAC;gBACjC,YAAY,CAAC,IAAI,CAAC,GAAG,cAAc,CAAC;gBAEpC,sCAAsC;gBACtC,MAAM,KAAK,GAAG,eAAe,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;gBACtD,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;oBACjB,eAAe,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;gBACnC,CAAC;YACH,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,8BAA8B;QAC9B,IAAI,SAAS,GAAG,CAAC,CAAC;QAClB,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;YAC1C,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;QACvC,CAAC,CAAC,CAAC;QAEH,yCAAyC;QACzC,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC/B,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,eAAe,CAAC,CAAC;QACtD,CAAC;QAED,8CAA8C;QAC9C,cAAc,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;YAC9B,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,SAAS,EAAE,CAAC;gBACrC,2EAA2E;gBAC3E,IAAI,cAAkC,CAAC;gBACvC,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,EAAE,MAAM,CAAC,EAAE,EAAE;oBAC7D,IAAI,WAAW,KAAK,IAAI,IAAI,eAAe,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;wBAC7D,cAAc,GAAG,MAAM,CAAC;oBAC1B,CAAC;gBACH,CAAC,CAAC,CAAC;gBAEH,IAAI,cAAc,KAAK,SAAS,EAAE,CAAC;oBACjC,0CAA0C;oBAC1C,YAAY,CAAC,IAAI,CAAC,GAAG,cAAc,CAAC;oBAEpC,4BAA4B;oBAC5B,MAAM,KAAK,GAAG,eAAe,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;oBACtD,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;wBACjB,eAAe,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;oBACnC,CAAC;gBACH,CAAC;qBAAM,CAAC;oBACN,6BAA6B;oBAC7B,IAAI,UAAU,GAAG,SAAS,GAAG,CAAC,CAAC;oBAC/B,OAAO,eAAe,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;wBAC5C,UAAU,EAAE,CAAC;oBACf,CAAC;oBAED,YAAY,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC;oBAChC,SAAS,GAAG,UAAU,CAAC;gBACzB,CAAC;YACH,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,6CAA6C;QAC7C,SAAS,CAAC,QAAQ,CAAC,CAAC,MAAM,GAAG,YAAY,CAAC;QAC1C,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC/B,SAAS,CAAC,QAAQ,CAAC,CAAC,eAAe,GAAG,eAAe,CAAC;QACxD,CAAC;aAAM,CAAC;YACN,+DAA+D;YAC/D,OAAO,SAAS,CAAC,QAAQ,CAAC,CAAC,eAAe,CAAC;QAC7C,CAAC;QAED,iDAAiD;QACjD,OAAO,CAAC,GAAG,cAAc,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;YACvC,OAAO,YAAY,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;QAC3C,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;;;;OAMG;IACI,0BAA0B,CAAC,WAAmB,EAAE,eAAyB;QAC9E,OAAO,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,WAAW,EAAE,eAAe,CAAC,CAAC;IACnF,CAAC;IAED;;;;;;OAMG;IACI,uBAAuB,CAAC,QAAgB,EAAE,eAAyB;QACxE,OAAO,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,eAAe,CAAC,CAAC;IAC7E,CAAC;IAED;;;;;;OAMG;IACI,sBAAsB,CAAC,SAAiB,EAAE,aAAuB;QACtE,uDAAuD;QACvD,OAAO,IAAI,CAAC,0BAA0B,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;IACnE,CAAC;IAED;;OAEG;IACI,WAAW;QAChB,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;CACF"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { GraphQLSchema } from 'graphql';
|
|
2
|
+
import { GRPCMapping } from '@wundergraph/cosmo-connect/dist/node/v1/node_pb';
|
|
3
|
+
/**
|
|
4
|
+
* Visitor that converts a GraphQL schema to gRPC mapping definitions
|
|
5
|
+
*
|
|
6
|
+
* This visitor traverses a GraphQL schema and generates mappings between:
|
|
7
|
+
* - GraphQL operations and gRPC RPC methods
|
|
8
|
+
* - GraphQL entity types (with @key directive) and corresponding lookup methods
|
|
9
|
+
* - GraphQL types/fields and Protocol Buffer message types/fields
|
|
10
|
+
* - GraphQL enums and Protocol Buffer enums
|
|
11
|
+
*
|
|
12
|
+
* The generated mappings are used to translate between GraphQL and Protocol Buffer
|
|
13
|
+
* representations in a consistent manner.
|
|
14
|
+
*/
|
|
15
|
+
export declare class GraphQLToProtoVisitor {
|
|
16
|
+
private readonly mapping;
|
|
17
|
+
private readonly schema;
|
|
18
|
+
/**
|
|
19
|
+
* Creates a new visitor for generating gRPC mappings from a GraphQL schema
|
|
20
|
+
*
|
|
21
|
+
* @param schema - The GraphQL schema to process
|
|
22
|
+
* @param serviceName - Name for the generated service (defaults to "DefaultService")
|
|
23
|
+
*/
|
|
24
|
+
constructor(schema: GraphQLSchema, serviceName?: string);
|
|
25
|
+
/**
|
|
26
|
+
* Process the GraphQL schema and generate all necessary mappings
|
|
27
|
+
*
|
|
28
|
+
* The processing order is important:
|
|
29
|
+
* 1. First entity types (with @key directives) are processed to identify federated entities
|
|
30
|
+
* 2. Then Query operations are processed to map GraphQL queries to RPC methods
|
|
31
|
+
* 3. Finally all remaining types are processed to ensure complete mapping coverage
|
|
32
|
+
*
|
|
33
|
+
* @returns The completed gRPC mapping definitions
|
|
34
|
+
*/
|
|
35
|
+
visit(): GRPCMapping;
|
|
36
|
+
/**
|
|
37
|
+
* Processes entity types (GraphQL types with @key directive)
|
|
38
|
+
*
|
|
39
|
+
* Federation entities require special handling to generate appropriate
|
|
40
|
+
* lookup RPC methods and entity mappings.
|
|
41
|
+
*/
|
|
42
|
+
private processEntityTypes;
|
|
43
|
+
/**
|
|
44
|
+
* Extract the key directive from a GraphQL object type
|
|
45
|
+
*
|
|
46
|
+
* @param type - The GraphQL object type to check for key directive
|
|
47
|
+
* @returns The key directive if found, undefined otherwise
|
|
48
|
+
*/
|
|
49
|
+
private getKeyDirective;
|
|
50
|
+
/**
|
|
51
|
+
* Creates an entity mapping for a federated entity type
|
|
52
|
+
*
|
|
53
|
+
* This defines how a GraphQL federated entity maps to a gRPC lookup method
|
|
54
|
+
* and its corresponding request/response messages.
|
|
55
|
+
*
|
|
56
|
+
* @param typeName - The name of the GraphQL entity type
|
|
57
|
+
* @param keyField - The field that serves as the entity's key
|
|
58
|
+
*/
|
|
59
|
+
private createEntityMapping;
|
|
60
|
+
/**
|
|
61
|
+
* Extract key fields from a @key directive
|
|
62
|
+
*
|
|
63
|
+
* The @key directive specifies which fields form the entity's primary key
|
|
64
|
+
* in Federation. This method extracts those field names.
|
|
65
|
+
*
|
|
66
|
+
* @param directive - The @key directive from the GraphQL AST
|
|
67
|
+
* @returns Array of field names that form the key
|
|
68
|
+
*/
|
|
69
|
+
private getKeyFieldsFromDirective;
|
|
70
|
+
/**
|
|
71
|
+
* Process the GraphQL Query type to generate query operation mappings
|
|
72
|
+
*
|
|
73
|
+
* Each field on the Query type represents a GraphQL query operation that
|
|
74
|
+
* needs to be mapped to a corresponding gRPC RPC method.
|
|
75
|
+
*/
|
|
76
|
+
private processQueryType;
|
|
77
|
+
/**
|
|
78
|
+
* Process the GraphQL Mutation type to generate mutation operation mappings
|
|
79
|
+
*
|
|
80
|
+
* Each field on the Mutation type represents a GraphQL mutation operation that
|
|
81
|
+
* needs to be mapped to a corresponding gRPC RPC method.
|
|
82
|
+
*/
|
|
83
|
+
private processMutationType;
|
|
84
|
+
/**
|
|
85
|
+
* Process the GraphQL Subscription type to generate subscription operation mappings
|
|
86
|
+
*
|
|
87
|
+
* Each field on the Subscription type represents a GraphQL subscription operation that
|
|
88
|
+
* needs to be mapped to a corresponding gRPC RPC method.
|
|
89
|
+
*/
|
|
90
|
+
private processSubscriptionType;
|
|
91
|
+
/**
|
|
92
|
+
* Process a GraphQL type to generate operation mappings
|
|
93
|
+
*
|
|
94
|
+
* This method processes a specific GraphQL type (e.g., Query, Mutation, Subscription)
|
|
95
|
+
* and generates mappings for its fields to corresponding gRPC RPC methods.
|
|
96
|
+
*
|
|
97
|
+
* @param operationTypeName - The name of the GraphQL type (Query, Mutation, Subscription)
|
|
98
|
+
* @param operationType - The type of operation (Query, Mutation, Subscription)
|
|
99
|
+
* @param graphqlType - The GraphQL type to process
|
|
100
|
+
*/
|
|
101
|
+
private processType;
|
|
102
|
+
/**
|
|
103
|
+
* Create an operation mapping between a GraphQL query and gRPC method
|
|
104
|
+
*
|
|
105
|
+
* @param operationType - The type of operation (Query, Mutation, Subscription)
|
|
106
|
+
* @param fieldName - Original GraphQL field name
|
|
107
|
+
* @param mappedName - Transformed name for use in gRPC context
|
|
108
|
+
*/
|
|
109
|
+
private createOperationMapping;
|
|
110
|
+
/**
|
|
111
|
+
* Process all remaining GraphQL types to generate complete mappings
|
|
112
|
+
*
|
|
113
|
+
* This ensures that all object types, input types, and enums in the schema
|
|
114
|
+
* have appropriate mappings for their fields and values.
|
|
115
|
+
*/
|
|
116
|
+
private processAllTypes;
|
|
117
|
+
/**
|
|
118
|
+
* Determines if a type should be skipped during processing
|
|
119
|
+
*
|
|
120
|
+
* We skip:
|
|
121
|
+
* - Built-in GraphQL types (prefixed with __)
|
|
122
|
+
* - Root operation types (Query, Mutation, Subscription)
|
|
123
|
+
*
|
|
124
|
+
* @param type - The GraphQL type to check
|
|
125
|
+
* @returns True if the type should be skipped, false otherwise
|
|
126
|
+
*/
|
|
127
|
+
private shouldSkipRootType;
|
|
128
|
+
/**
|
|
129
|
+
* Process a GraphQL object type to generate field mappings
|
|
130
|
+
*
|
|
131
|
+
* @param type - The GraphQL object type to process
|
|
132
|
+
*/
|
|
133
|
+
private processObjectType;
|
|
134
|
+
/**
|
|
135
|
+
* Process a GraphQL input object type to generate field mappings
|
|
136
|
+
*
|
|
137
|
+
* Input objects are handled separately because they have different
|
|
138
|
+
* field structures than regular object types.
|
|
139
|
+
*
|
|
140
|
+
* @param type - The GraphQL input object type to process
|
|
141
|
+
*/
|
|
142
|
+
private processInputObjectType;
|
|
143
|
+
/**
|
|
144
|
+
* Process a GraphQL enum type to generate value mappings
|
|
145
|
+
*
|
|
146
|
+
* GraphQL enums are mapped to Protocol Buffer enums with appropriate
|
|
147
|
+
* naming conventions for the enum values.
|
|
148
|
+
*
|
|
149
|
+
* @param type - The GraphQL enum type to process
|
|
150
|
+
*/
|
|
151
|
+
private processEnumType;
|
|
152
|
+
/**
|
|
153
|
+
* Create a field mapping between a GraphQL field and Protocol Buffer field
|
|
154
|
+
*
|
|
155
|
+
* This includes mapping the field name and any arguments the field may have.
|
|
156
|
+
*
|
|
157
|
+
* @param type - The name of the containing GraphQL type
|
|
158
|
+
* @param field - The GraphQL field to create a mapping for
|
|
159
|
+
* @returns The created field mapping
|
|
160
|
+
*/
|
|
161
|
+
private createFieldMapping;
|
|
162
|
+
/**
|
|
163
|
+
* Create argument mappings for a GraphQL field
|
|
164
|
+
*
|
|
165
|
+
* Maps each argument to its Protocol Buffer representation with
|
|
166
|
+
* appropriate naming conventions.
|
|
167
|
+
*
|
|
168
|
+
* @param field - The GraphQL field containing arguments
|
|
169
|
+
* @returns Array of argument mappings
|
|
170
|
+
*/
|
|
171
|
+
private createArgumentMappings;
|
|
172
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { isEnumType, isInputObjectType, isObjectType, Kind, } from 'graphql';
|
|
2
|
+
import { createEntityLookupMethodName, createEntityLookupRequestName, createEntityLookupResponseName, createOperationMethodName, createRequestMessageName, createResponseMessageName, graphqlArgumentToProtoField, graphqlEnumValueToProtoEnumValue, graphqlFieldToProtoField, } from './naming-conventions.js';
|
|
3
|
+
import { ArgumentMapping, EntityMapping, EnumMapping, EnumValueMapping, FieldMapping, GRPCMapping, OperationMapping, OperationType, TypeFieldMapping, } from '@wundergraph/cosmo-connect/dist/node/v1/node_pb';
|
|
4
|
+
/**
|
|
5
|
+
* Visitor that converts a GraphQL schema to gRPC mapping definitions
|
|
6
|
+
*
|
|
7
|
+
* This visitor traverses a GraphQL schema and generates mappings between:
|
|
8
|
+
* - GraphQL operations and gRPC RPC methods
|
|
9
|
+
* - GraphQL entity types (with @key directive) and corresponding lookup methods
|
|
10
|
+
* - GraphQL types/fields and Protocol Buffer message types/fields
|
|
11
|
+
* - GraphQL enums and Protocol Buffer enums
|
|
12
|
+
*
|
|
13
|
+
* The generated mappings are used to translate between GraphQL and Protocol Buffer
|
|
14
|
+
* representations in a consistent manner.
|
|
15
|
+
*/
|
|
16
|
+
export class GraphQLToProtoVisitor {
|
|
17
|
+
/**
|
|
18
|
+
* Creates a new visitor for generating gRPC mappings from a GraphQL schema
|
|
19
|
+
*
|
|
20
|
+
* @param schema - The GraphQL schema to process
|
|
21
|
+
* @param serviceName - Name for the generated service (defaults to "DefaultService")
|
|
22
|
+
*/
|
|
23
|
+
constructor(schema, serviceName = 'DefaultService') {
|
|
24
|
+
this.schema = schema;
|
|
25
|
+
this.mapping = new GRPCMapping({
|
|
26
|
+
version: 1,
|
|
27
|
+
service: serviceName,
|
|
28
|
+
operationMappings: [],
|
|
29
|
+
entityMappings: [],
|
|
30
|
+
typeFieldMappings: [],
|
|
31
|
+
enumMappings: [],
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Process the GraphQL schema and generate all necessary mappings
|
|
36
|
+
*
|
|
37
|
+
* The processing order is important:
|
|
38
|
+
* 1. First entity types (with @key directives) are processed to identify federated entities
|
|
39
|
+
* 2. Then Query operations are processed to map GraphQL queries to RPC methods
|
|
40
|
+
* 3. Finally all remaining types are processed to ensure complete mapping coverage
|
|
41
|
+
*
|
|
42
|
+
* @returns The completed gRPC mapping definitions
|
|
43
|
+
*/
|
|
44
|
+
visit() {
|
|
45
|
+
// Process entity types first (types with @key directive)
|
|
46
|
+
this.processEntityTypes();
|
|
47
|
+
// Process query type
|
|
48
|
+
this.processQueryType();
|
|
49
|
+
// Process mutation type
|
|
50
|
+
this.processMutationType();
|
|
51
|
+
// Process subscription type
|
|
52
|
+
this.processSubscriptionType();
|
|
53
|
+
// Process all other types for field mappings
|
|
54
|
+
this.processAllTypes();
|
|
55
|
+
return this.mapping;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Processes entity types (GraphQL types with @key directive)
|
|
59
|
+
*
|
|
60
|
+
* Federation entities require special handling to generate appropriate
|
|
61
|
+
* lookup RPC methods and entity mappings.
|
|
62
|
+
*/
|
|
63
|
+
processEntityTypes() {
|
|
64
|
+
const typeMap = this.schema.getTypeMap();
|
|
65
|
+
for (const typeName in typeMap) {
|
|
66
|
+
const type = typeMap[typeName];
|
|
67
|
+
// Skip built-in types and query/mutation/subscription types
|
|
68
|
+
if (this.shouldSkipRootType(type))
|
|
69
|
+
continue;
|
|
70
|
+
// Check if this is an entity type (has @key directive)
|
|
71
|
+
if (isObjectType(type)) {
|
|
72
|
+
const keyDirective = this.getKeyDirective(type);
|
|
73
|
+
if (!keyDirective)
|
|
74
|
+
continue;
|
|
75
|
+
const keyFields = this.getKeyFieldsFromDirective(keyDirective);
|
|
76
|
+
if (keyFields.length > 0) {
|
|
77
|
+
// Create entity mapping using the first key field
|
|
78
|
+
this.createEntityMapping(typeName, keyFields[0]);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Extract the key directive from a GraphQL object type
|
|
85
|
+
*
|
|
86
|
+
* @param type - The GraphQL object type to check for key directive
|
|
87
|
+
* @returns The key directive if found, undefined otherwise
|
|
88
|
+
*/
|
|
89
|
+
getKeyDirective(type) {
|
|
90
|
+
var _a, _b;
|
|
91
|
+
return (_b = (_a = type.astNode) === null || _a === void 0 ? void 0 : _a.directives) === null || _b === void 0 ? void 0 : _b.find((d) => d.name.value === 'key');
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Creates an entity mapping for a federated entity type
|
|
95
|
+
*
|
|
96
|
+
* This defines how a GraphQL federated entity maps to a gRPC lookup method
|
|
97
|
+
* and its corresponding request/response messages.
|
|
98
|
+
*
|
|
99
|
+
* @param typeName - The name of the GraphQL entity type
|
|
100
|
+
* @param keyField - The field that serves as the entity's key
|
|
101
|
+
*/
|
|
102
|
+
createEntityMapping(typeName, keyField) {
|
|
103
|
+
const entityMapping = new EntityMapping({
|
|
104
|
+
typeName,
|
|
105
|
+
kind: 'entity',
|
|
106
|
+
key: keyField,
|
|
107
|
+
rpc: createEntityLookupMethodName(typeName, keyField),
|
|
108
|
+
request: createEntityLookupRequestName(typeName, keyField),
|
|
109
|
+
response: createEntityLookupResponseName(typeName, keyField),
|
|
110
|
+
});
|
|
111
|
+
this.mapping.entityMappings.push(entityMapping);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Extract key fields from a @key directive
|
|
115
|
+
*
|
|
116
|
+
* The @key directive specifies which fields form the entity's primary key
|
|
117
|
+
* in Federation. This method extracts those field names.
|
|
118
|
+
*
|
|
119
|
+
* @param directive - The @key directive from the GraphQL AST
|
|
120
|
+
* @returns Array of field names that form the key
|
|
121
|
+
*/
|
|
122
|
+
getKeyFieldsFromDirective(directive) {
|
|
123
|
+
var _a;
|
|
124
|
+
// Extract fields argument from the key directive
|
|
125
|
+
const fieldsArg = (_a = directive.arguments) === null || _a === void 0 ? void 0 : _a.find((arg) => arg.name.value === 'fields');
|
|
126
|
+
if (fieldsArg && fieldsArg.value.kind === Kind.STRING) {
|
|
127
|
+
return fieldsArg.value.value.split(' ');
|
|
128
|
+
}
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Process the GraphQL Query type to generate query operation mappings
|
|
133
|
+
*
|
|
134
|
+
* Each field on the Query type represents a GraphQL query operation that
|
|
135
|
+
* needs to be mapped to a corresponding gRPC RPC method.
|
|
136
|
+
*/
|
|
137
|
+
processQueryType() {
|
|
138
|
+
this.processType('Query', OperationType.QUERY, this.schema.getQueryType());
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Process the GraphQL Mutation type to generate mutation operation mappings
|
|
142
|
+
*
|
|
143
|
+
* Each field on the Mutation type represents a GraphQL mutation operation that
|
|
144
|
+
* needs to be mapped to a corresponding gRPC RPC method.
|
|
145
|
+
*/
|
|
146
|
+
processMutationType() {
|
|
147
|
+
this.processType('Mutation', OperationType.MUTATION, this.schema.getMutationType());
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Process the GraphQL Subscription type to generate subscription operation mappings
|
|
151
|
+
*
|
|
152
|
+
* Each field on the Subscription type represents a GraphQL subscription operation that
|
|
153
|
+
* needs to be mapped to a corresponding gRPC RPC method.
|
|
154
|
+
*/
|
|
155
|
+
processSubscriptionType() {
|
|
156
|
+
this.processType('Subscription', OperationType.SUBSCRIPTION, this.schema.getSubscriptionType());
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Process a GraphQL type to generate operation mappings
|
|
160
|
+
*
|
|
161
|
+
* This method processes a specific GraphQL type (e.g., Query, Mutation, Subscription)
|
|
162
|
+
* and generates mappings for its fields to corresponding gRPC RPC methods.
|
|
163
|
+
*
|
|
164
|
+
* @param operationTypeName - The name of the GraphQL type (Query, Mutation, Subscription)
|
|
165
|
+
* @param operationType - The type of operation (Query, Mutation, Subscription)
|
|
166
|
+
* @param graphqlType - The GraphQL type to process
|
|
167
|
+
*/
|
|
168
|
+
processType(operationTypeName, operationType, graphqlType) {
|
|
169
|
+
if (!graphqlType)
|
|
170
|
+
return;
|
|
171
|
+
const typeFieldMapping = new TypeFieldMapping({
|
|
172
|
+
type: operationTypeName,
|
|
173
|
+
fieldMappings: [],
|
|
174
|
+
});
|
|
175
|
+
const fields = graphqlType.getFields();
|
|
176
|
+
for (const fieldName in fields) {
|
|
177
|
+
// Skip special federation fields
|
|
178
|
+
if (fieldName === '_entities')
|
|
179
|
+
continue;
|
|
180
|
+
const field = fields[fieldName];
|
|
181
|
+
const mappedName = createOperationMethodName(operationTypeName, fieldName);
|
|
182
|
+
this.createOperationMapping(operationType, fieldName, mappedName);
|
|
183
|
+
const fieldMapping = this.createFieldMapping(operationTypeName, field);
|
|
184
|
+
typeFieldMapping.fieldMappings.push(fieldMapping);
|
|
185
|
+
}
|
|
186
|
+
this.mapping.typeFieldMappings.push(typeFieldMapping);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Create an operation mapping between a GraphQL query and gRPC method
|
|
190
|
+
*
|
|
191
|
+
* @param operationType - The type of operation (Query, Mutation, Subscription)
|
|
192
|
+
* @param fieldName - Original GraphQL field name
|
|
193
|
+
* @param mappedName - Transformed name for use in gRPC context
|
|
194
|
+
*/
|
|
195
|
+
createOperationMapping(operationType, fieldName, mappedName) {
|
|
196
|
+
const operationMapping = new OperationMapping({
|
|
197
|
+
type: operationType,
|
|
198
|
+
original: fieldName,
|
|
199
|
+
mapped: mappedName,
|
|
200
|
+
request: createRequestMessageName(mappedName),
|
|
201
|
+
response: createResponseMessageName(mappedName),
|
|
202
|
+
});
|
|
203
|
+
this.mapping.operationMappings.push(operationMapping);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Process all remaining GraphQL types to generate complete mappings
|
|
207
|
+
*
|
|
208
|
+
* This ensures that all object types, input types, and enums in the schema
|
|
209
|
+
* have appropriate mappings for their fields and values.
|
|
210
|
+
*/
|
|
211
|
+
processAllTypes() {
|
|
212
|
+
const typeMap = this.schema.getTypeMap();
|
|
213
|
+
for (const typeName in typeMap) {
|
|
214
|
+
const type = typeMap[typeName];
|
|
215
|
+
if (this.shouldSkipRootType(type))
|
|
216
|
+
continue;
|
|
217
|
+
// Process each type according to its kind
|
|
218
|
+
if (isObjectType(type)) {
|
|
219
|
+
this.processObjectType(type);
|
|
220
|
+
}
|
|
221
|
+
else if (isInputObjectType(type)) {
|
|
222
|
+
this.processInputObjectType(type);
|
|
223
|
+
}
|
|
224
|
+
else if (isEnumType(type)) {
|
|
225
|
+
this.processEnumType(type);
|
|
226
|
+
}
|
|
227
|
+
// Note: Union types don't need field mappings in our implementation
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Determines if a type should be skipped during processing
|
|
232
|
+
*
|
|
233
|
+
* We skip:
|
|
234
|
+
* - Built-in GraphQL types (prefixed with __)
|
|
235
|
+
* - Root operation types (Query, Mutation, Subscription)
|
|
236
|
+
*
|
|
237
|
+
* @param type - The GraphQL type to check
|
|
238
|
+
* @returns True if the type should be skipped, false otherwise
|
|
239
|
+
*/
|
|
240
|
+
shouldSkipRootType(type) {
|
|
241
|
+
var _a, _b, _c;
|
|
242
|
+
const typeName = type.name;
|
|
243
|
+
return (typeName.startsWith('__') ||
|
|
244
|
+
typeName === ((_a = this.schema.getQueryType()) === null || _a === void 0 ? void 0 : _a.name) ||
|
|
245
|
+
typeName === ((_b = this.schema.getMutationType()) === null || _b === void 0 ? void 0 : _b.name) ||
|
|
246
|
+
typeName === ((_c = this.schema.getSubscriptionType()) === null || _c === void 0 ? void 0 : _c.name));
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Process a GraphQL object type to generate field mappings
|
|
250
|
+
*
|
|
251
|
+
* @param type - The GraphQL object type to process
|
|
252
|
+
*/
|
|
253
|
+
processObjectType(type) {
|
|
254
|
+
const typeFieldMapping = new TypeFieldMapping({
|
|
255
|
+
type: type.name,
|
|
256
|
+
fieldMappings: [],
|
|
257
|
+
});
|
|
258
|
+
const fields = type.getFields();
|
|
259
|
+
for (const fieldName in fields) {
|
|
260
|
+
const field = fields[fieldName];
|
|
261
|
+
const fieldMapping = this.createFieldMapping(type.name, field);
|
|
262
|
+
typeFieldMapping.fieldMappings.push(fieldMapping);
|
|
263
|
+
}
|
|
264
|
+
// Only add to mappings if there are fields to map
|
|
265
|
+
if (typeFieldMapping.fieldMappings.length > 0) {
|
|
266
|
+
this.mapping.typeFieldMappings.push(typeFieldMapping);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Process a GraphQL input object type to generate field mappings
|
|
271
|
+
*
|
|
272
|
+
* Input objects are handled separately because they have different
|
|
273
|
+
* field structures than regular object types.
|
|
274
|
+
*
|
|
275
|
+
* @param type - The GraphQL input object type to process
|
|
276
|
+
*/
|
|
277
|
+
processInputObjectType(type) {
|
|
278
|
+
const typeFieldMapping = new TypeFieldMapping({
|
|
279
|
+
type: type.name,
|
|
280
|
+
fieldMappings: [],
|
|
281
|
+
});
|
|
282
|
+
const fields = type.getFields();
|
|
283
|
+
for (const fieldName in fields) {
|
|
284
|
+
const field = fields[fieldName];
|
|
285
|
+
// Input fields don't have args, so we create a simpler field mapping
|
|
286
|
+
const fieldMapping = new FieldMapping({
|
|
287
|
+
original: field.name,
|
|
288
|
+
mapped: graphqlFieldToProtoField(field.name),
|
|
289
|
+
argumentMappings: [],
|
|
290
|
+
});
|
|
291
|
+
typeFieldMapping.fieldMappings.push(fieldMapping);
|
|
292
|
+
}
|
|
293
|
+
// Only add to mappings if there are fields to map
|
|
294
|
+
if (typeFieldMapping.fieldMappings.length > 0) {
|
|
295
|
+
this.mapping.typeFieldMappings.push(typeFieldMapping);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Process a GraphQL enum type to generate value mappings
|
|
300
|
+
*
|
|
301
|
+
* GraphQL enums are mapped to Protocol Buffer enums with appropriate
|
|
302
|
+
* naming conventions for the enum values.
|
|
303
|
+
*
|
|
304
|
+
* @param type - The GraphQL enum type to process
|
|
305
|
+
*/
|
|
306
|
+
processEnumType(type) {
|
|
307
|
+
const enumMapping = new EnumMapping({
|
|
308
|
+
type: type.name,
|
|
309
|
+
values: [],
|
|
310
|
+
});
|
|
311
|
+
const enumValues = type.getValues();
|
|
312
|
+
// Map each enum value to its Protocol Buffer representation
|
|
313
|
+
for (const enumValue of enumValues) {
|
|
314
|
+
enumMapping.values.push(new EnumValueMapping({
|
|
315
|
+
original: enumValue.name,
|
|
316
|
+
// Convert to UPPER_SNAKE_CASE with type name prefix for Proto enums
|
|
317
|
+
mapped: graphqlEnumValueToProtoEnumValue(type.name, enumValue.name),
|
|
318
|
+
}));
|
|
319
|
+
}
|
|
320
|
+
this.mapping.enumMappings.push(enumMapping);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Create a field mapping between a GraphQL field and Protocol Buffer field
|
|
324
|
+
*
|
|
325
|
+
* This includes mapping the field name and any arguments the field may have.
|
|
326
|
+
*
|
|
327
|
+
* @param type - The name of the containing GraphQL type
|
|
328
|
+
* @param field - The GraphQL field to create a mapping for
|
|
329
|
+
* @returns The created field mapping
|
|
330
|
+
*/
|
|
331
|
+
createFieldMapping(type, field) {
|
|
332
|
+
const fieldName = field.name;
|
|
333
|
+
// Convert field names to snake_case for Protocol Buffers
|
|
334
|
+
const mappedFieldName = graphqlFieldToProtoField(fieldName);
|
|
335
|
+
const argumentMappings = this.createArgumentMappings(field);
|
|
336
|
+
return new FieldMapping({
|
|
337
|
+
original: fieldName,
|
|
338
|
+
mapped: mappedFieldName,
|
|
339
|
+
argumentMappings,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Create argument mappings for a GraphQL field
|
|
344
|
+
*
|
|
345
|
+
* Maps each argument to its Protocol Buffer representation with
|
|
346
|
+
* appropriate naming conventions.
|
|
347
|
+
*
|
|
348
|
+
* @param field - The GraphQL field containing arguments
|
|
349
|
+
* @returns Array of argument mappings
|
|
350
|
+
*/
|
|
351
|
+
createArgumentMappings(field) {
|
|
352
|
+
const argumentMappings = [];
|
|
353
|
+
if (field.args && field.args.length > 0) {
|
|
354
|
+
for (const arg of field.args) {
|
|
355
|
+
argumentMappings.push(new ArgumentMapping({
|
|
356
|
+
original: arg.name,
|
|
357
|
+
// Convert argument names to snake_case for Protocol Buffers
|
|
358
|
+
mapped: graphqlArgumentToProtoField(arg.name),
|
|
359
|
+
}));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return argumentMappings;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
//# sourceMappingURL=sdl-to-mapping-visitor.js.map
|