-
Notifications
You must be signed in to change notification settings - Fork 226
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
GraphQL Emitter Design #4933
Comments
Thanks for sharing the design doc for the GraphQL emitter, it looks solid overall! I'll defer to @bterlson for specific feedback on the design, here are a couple of high-level suggestions from me as well: 1. Error HandlingCan you include more details on error handling throughout the process? For example, specify how errors during type resolution or schema validation are managed. 2. Example WorkflowA step-by-step example of processing a simple TypeSpec definition, from initialization to schema generation would be super helpful. 3. TestingI'd love to see the strategy for testing folded into the design doc, with a focus on:
Great work so far, I'm looking forward to seeing the final product! |
Sorry for the delay in getting to this, crazy times followed by vacation. Overall I like the proposed design and the architectural sketch, though for the latter feel free to change that as you build as it's less important. Next steps I think we should make any changes you want to make based on the below feedback and schedule a design meeting in the next week or two to go over some of these details and have any needed discussion. Thoughts:
|
Thanks both! I'll quickly respond to @bterlson 's questions here:
Yes, they are. We just wanted to present both, but we're leaning towards |
Sounds good, I agree with that assessment I think. How does the 18th at 10am America/Los_Angeles sound for a final review of this proposal with the TypeSpec design crew? |
GraphQL Emitter Design
Authors: Angel Vargas, Steve Rice (He Him), Swati Kumar
Last updated: Oct 31, 2024
Motivation
From the TypeSpec docs:
TypeSpec's standard library includes support for emitting OpenAPI 3.0, JSON Schema 2020-12 and Protobuf.
As GraphQL is a widely used protocol for querying data by client applications, providing GraphQL support in the TypeSpec standard library can help bring all valuable TypeSpec features to the GraphQL ecosystem.
This proposal describes the design for a GraphQL emitter that can be added to TypeSpec's standard library and can be used to emit a valid GraphQL schema from a valid TypeSpec definition.
General Emitter Design Guidelines
Refer to 4604
GraphQL spec and validation rules
_
orletter
)invisibility
decorator to define another field with a GraphQL valid name@specifiedBy
directive or thespecifiedByURL
introspection field that must link to a human readable specification of data format, serialization, and coercion rules for the scalar@specifiedBy
directive in the schema to point to themBasic emitter building blocks
The following building blocks are used by the emitter code:
The main design constraint is that we only want to traverse the TSP program once to collect and build the GraphQL types.
Detailed Emitter Design
Design Scenarios
We need to consider two main scenarios when designing the GraphQL emitter:
@route
) to the existing TypeSpec code used to generate the GraphQL schema and the existing graphql emitter should continue to work as expected.Any
scalars for unsupported GraphQL objects and emitting all the operations in the TypeSpec code. Although the emitted GraphQL schema might lack optimal design, it remains functional. If a specific pattern can enhance the GraphQL schema and aligns with our design guidelines, it should be applied. We will also offer warnings and recommendations to assist developers in modifying the TypeSpec code to improve their emitted GraphQL schema incrementally.Output Types
Context and design challenges
GraphQL distinguishes between Input and Output Types. While there is no way in TypeSpec to allow the developers to specify this, the compiler provides a mechanism that identifies each model as Input and/or Output using
UsageFlags
.In GraphQL:
Scalar
andEnum
types can be used as both: Input and OutputObject
,Interface
andUnion
types can be used only as OutputInput Object
types can't be used as OutputDesign Proposal
Use the
UsageFlags
to identify theinput
andoutput
types for GraphQL.🔴 Design Decision: As TSP will allow a model to be both
input
andoutput
type and indeed that would be useful for GraphQL as well, the GraphQL emitter will support this case. In order to differentiate between theinput
andoutput
types we propose creating a new GraphQL type for inputs with the name of the type +Input
suffix.When creating an operation that returns models, all directly or indirectly referenced models, should be emitted as valid GraphQL output types.
Mapping
Examples
More complicated examples with unions, interfaces, and lists are described in their respective sections.
Input Types
Context and design challenges
Use the
UsageFlags.INPUT
to determine if a TSPmodel
is aninput
type. The following validation rules apply toinput
types:input
type.Input
types may not be defined as an unbroken chain of Non-Null singular fields as shown belowoptional
input type, anull
value can be provided, and that would be assigned to this type.Optional
input types can also be “missing” from the input map.Null
andmissing
are treated differently.Design Proposal
To emit a valid GraphQL and still represent the schema defined in TypeSpec, the emitter will follow these rules:
Scalar
orEnum
, the type is generated normally.Model
and all the properties of theModel
are of valid Input types, a newInput
object will be created in GraphQL, with the typename as the original type +Input
suffix.Input
suffix regardless of whether or not it is used as both, because the model can be used as bothinput
andoutput
in the future and changing the type name will cause issues with schema evolution.Input
suffix can be annoying or result in types likeUserInputInput
model
or its properties are invalid Input types, the type of the invalid model or property will be assigned to theAny
scalar type and a warning will be emitted.model
contains an unbroken chain of non-null singular fields, throw an error and fail the emitter processMapping
Examples
Translate the invalid input to Any
Throw an error in emitter validation
Design Alternatives
For specifying GraphQL/HTTP specific types:
invisible
andvisible
Auto-resolve unwrapping of unions
@invisible
decorator applied tounion variants
, the emitter creators will have to deal with the auto-unwrapping of unions with just one variant. As this would be common functionality to all emitters, perhaps this should be done in a common place like by the TSP compilerScalars
Context and design challenges
GraphQL only provides five built-in scalars: Int, String, Float, Boolean and ID.
Any other scalar should be added as a custom scalar, and the @SpecifiedBy directive should be added to provide a specification.
The ID scalar type represents an unique identifier, as defined here.
Design Proposal
The emitter will use the mappings provided below to map TypeSpec to GraphQL scalars, trying to emit as a built-in scalar when possible.
For the custom scalars, if the TypeSpec documentation mentions a specification, that will be used for the @SpecifiedBy directive. If not provided, we will use a link to the TypeSpec documentation: https://typespec.io/docs/standard-library/built-in-data-types/
Encodings provided by the @encode decorator in TSP code would also be considered to build the proper custom scalar.
We are proposing a new TypeSpec native decorator @SpecifiedBy over scalars to allow developers to provide their own references. If provided, the emitter will use the information to generate the GraphQL directive.
To handle the ID type, the emitters library will include a TypeSpec scalar:
Type Mappings to GraphQL Built-In Scalars
Type Mappings to GraphQL custom Scalars
Examples
Unions
Context and design challenges
Design Proposal
Generate 1:1 mapping for regular unions.
For nested unions, a single union will be recursively composed with all the variants implicitly defined in TypeSpec.
As the
interface
models are decorated with an@Interface
decorator, throw a validation error when defining aunion
variant for a model type that is decorated with this.Wrap the scalars in a wrapping object type and emit a union with those types.
Create explicit unions in GraphQL for anonymous TSP unions, naming them using the context where the Union is declared, for example using model and property names, or the operation and parameter names, or the operation name if used as a return type. And all cases with the "Union" suffix. (See examples). Note that this approach may generate identical GraphQL unions with distinct names. We will throw an error if there are naming conflicts.
There are some special cases with distinct treatments, like:
null
type: see NullabilityMapping
Examples
Nested unions
Anonymous union in param
Named union of scalars
Named union of scalars and models
Anonymous union in return type
Design Alternatives
Union of scalars design alternative:
Any
type.Any
typesOpen Questions
Field Arguments
Context and design challenges
Design Proposal
operationFields
that referencesoperations
orinterfaces
to be added to a modeloperationFields
decorator are not emitted as part of the root GraphQL operations likequery
,mutation
, orsubscription
Mapping
Decorators
Examples
Additional examples that show namespaces in GraphQL can be found here:
Design Alternatives
@parameters({arg1: type1; arg2: type2;})
decorator targeting Model Properties.We prototyped this, but found issues when validating/generating the Input types.
@mapArguments(modelProperty, arg1, agr2, …)
decorator over Operations, where arg1, arg2, etc. are the name of the parameters of the target operation to map as arguments of the modelProperty.** [DISCARDED] ModelRoute Decorator Design:**
We propose to introduce a decorator over the
Operations
to map the operation as a new parameterized field of a model.@modelRoute
decorator will receive a parameter with the Model where to add the field.Query
andMutation
types.@modelRoute
decorator would be probably useless for other schemas because of the lack of the Model context; we may want to force the Model to appear in the parameters and exclude it from the GraphQL field arguments, or even take the first parameter of the operation as the Model.Interfaces
Context and design challenges
There is no way to represent GraphQL Interfaces in TSP directly. We’ll use a combination of special decorators and the spread operator to achieve this for the GraphQL emitter.
Only Output Types can be decorated as an
Interface
. If anInput Type
is decorated as anInterface
, a decorator validation error must be thrown.Design Proposal
GraphQL Interfaces will be defined using the two specific decorators outlined below:
The
@Interface
decorator will designate the TSP model to be used as an Interface in GraphQL. This model will be emitted as theGraphQLInterface
type.The
@compose
decorator designates whichInterface
s should the current model be composed of. The@compose
decorator can only refer to other models that are marked with the@Interface
decorator and not vanilla model types.Mapping
Decorators
compose
must be present in the target modelExamples
Fields within the composed model can be defined using either
...
operator or manually, both are validGraphQL requires both Person and Node to be explicitly implemented by Actor.
Design Alternatives
compose
automatically – this wouldn’t be great because thencompose
would change the shape of the model just for GraphQLInterface
and assume interfaces from models used incompose
. Since GraphQL has an explicit concept ofInterface
we’re representing that using this decorator. If validation rules specific toInterface
s need to be applied in the future, it will be possible to do soEnums
Context and design challenges
TSP enum member types have no meaning in GraphQL and the enum member values should follow the naming convention shown below (similar to all other literal names). From the GraphQL spec: “EnumValue
Name but not true false null”
where Name should start with [A-Za-z] or <underscore> and can be followed by letter, digit, or <underscore>
GraphQL Recommendation: “It is recommended that Enum values be “all caps”. Enum values are only used in contexts where the precise enumeration type is known. Therefore it’s not necessary to supply an enumeration type name in the literal.”
Design Proposal
Use TypeSpec enums in the value context as GraphQL doesn’t need the type information.
TypeSpec enums with no types that can only be identifiers or string literals will be translated to all caps GraphQL enums as long as the identifiers are valid GraphQL names. If they are invalid, the emitter will throw a validation error.
🔴 Design decision: TypeSpec enums with integer or floating point values will be converted to a string value using the following rules to create
result
:result
to_
NEGATIVE_
to the result string.
is converted to an_
result
Pros: The GraphQL enum is a string representation of the
value
and reflects the true intention of the developerCons: The server side implementation will have to figure out the translation between the GraphQL enum and the internal representation of the enum where the algorithm isn’t obvious (i.e. they will basically have to implement the steps above).
Inline enums that don’t have an enum name will be assigned a distinct name based on where the field appears in the TSP schema. The name derived from the field will be followed by an
Enum
suffix. To provide disambiguation, the full name should benamespace
+modelName
+fieldName
. See the examples table for an example.Mapping
Examples
Convert the hour values into GraphQL enum values
Note that we don’t use the type as TSP types might only have meaning within the TSP code and not the emitted protocol
Convert Boundary values into GraphQL enum values
Derive a unique name based on the namespace, model, field name \+ “Enum”
Design Alternatives
Any
for enums with values as integers or floating points and let the developer define an alternate type using the upcoming visibility redesign to provide an alternative definition.@invisible
decorator can be applied toEnumMembers
, we can provide alternate enum members for GraphQL in the same enum definition which change the emitter to emit the GraphQL enum values as shown below:Operations
Context and design challenges
There are three kinds of GraphQL Operations: Query, Mutation and Subscription. While in TypeSpec there is no difference between them.
Design Proposal
To distinguish between Queries, Mutations and Subscription, we are proposing to include a set of three decorators in TypeSpec: @query, @mutation and @subscription. These will decorate the TSP Operations to indicate the GraphQL kind.
The decorators would also be added to an interface, understanding that all operations within the interface would be of the provided kind.
The GraphQL emitter will generate the proper GraphQL kind for each Operation, according to these rules:
query,
post
.The Operation parameters will be converted to GraphQL arguments following the rules for the GraphQL Input types.
The Operation return type should be a valid GraphQL Output Type.
In line with the Field Arguments design, the operations decorated directly or indirectly with the @operationFields decorator, would not be added as query, mutations or subscriptions.
When no operation is emitted, an empty schema will be generated.
When mutations are provided, but there are no query operations, a dummy Query will be added to the schema to make it valid.
Mapping
Decorators
Examples
Decorator Validation Errors
Lists
Context and design challenges
TSP defines a
list
andArray
builtin types and both of those need to be converted to GraphQL lists. GraphQL lists are wrappers over output and input types.Design Proposal
For TSP lists (
[]
) and arrays (Array
) used as types of properties, parameters and operations, we will emit the corresponding list of types in GraphQL.Mapping
Examples
Note the difference in the requiredness of the values vs the list itself for the various options
Nullable vs Optional
Context and design challenges
In GraphQL, all properties and parameters are nullable by default, and the
!
operator is applied to indicate non-nullability.And although all fields are optional; for parameters, Input fields are required if they are marked as non-nullable.
In TypeSpec non-nullable is the default, while nullability is expressed by an Union that includes the
null
type. Also in TypeSpec: all the fields are required, unless are marked optional with the?
operator.Design Proposal
All output types and return types will be emitted in GraphQL as non-nullable (
!
operator), except when the field is marked as optional, or when the type of the field is an Union containing the TypeSpecnull
type.We can also use the same rules for Input fields, but we will force the field as required if the property or the argument is not nullable. Alternatively, we can throw an error.
Examples
Design Alternatives
Visibility & Never
Context and design challenges
@visibilty
and@witthVisibility
decorators.never
type@withVisibility
is already considered in the compiler, so it will be also included in the emitter.never
.Design Proposal
Add to the emitter the handling of the
never
type, and exclude any field from the Model before emitting the Model.Note: This may result in empty models. We need to define what to do with fields pointing to empty Models.
For Implicit filtered models (automatic visibility):
Note that the naming should include the Input suffix and this approach will generate models like UserCreateInput, UserUpdateInput, UserDeleteInput, etc.
Examples
Open Questions
User feedback:
The emitter will generate feedback for the developers through errors and warnings. But the warning list could be enormous and not easy to read, especially when trying to emit a GraphQL from a large TSP specification not specifically designed for GraphQL.
With this in mind we are proposing to emit a "How to improve your TypeSpec scheme for GraphQL" report based on the warnings and other signals. The purpose is to help developers to generate a better GraphQL schema, introducing the GraphQL decorators and other tricks to their TypeSpec code. The report should be more readable than the warnings.
Typespec extension suggestions
These will be opened as separate issues.
The text was updated successfully, but these errors were encountered: