Skip to content

Commit 1451ab2

Browse files
committed
visibility updates
1 parent 088e29a commit 1451ab2

1 file changed

Lines changed: 42 additions & 24 deletions

File tree

graphql_design_doc.md

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ The main design constraint is that we only want to traverse the TSP program once
7070
We need to consider two main scenarios when designing the GraphQL emitter:
7171

7272
1. When the TypeSpec code is specifically designed for emitting GraphQL, we can equip developers with GraphQL-specific decorators, and objects. This will aid in crafting TypeSpec code that generates well-designed GraphQL schemas. Given that GraphQL does not employ HTTP or REST concepts, developers should be able to bypass those libraries. However, it should still be feasible to emit OpenAPI or any other schema by adding the appropriate decorators (like `@route`) to the existing TypeSpec code used to generate the GraphQL schema and the existing graphql emitter should continue to work as expected.
73-
2. When a developer aims to create a GraphQL service from an existing TypeSpec schema originally used for emitters like OpenAPI, we focus on designing a usable GraphQL schema. This may involve using `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.
73+
2. When a developer aims to create a GraphQL service from an existing TypeSpec schema originally used for emitters like OpenAPI, we focus on producing a GraphQL schema that represents the TypeSpec with no loss of specificity. Instead of assuming intent, we will provide errors and warnings as soon as possible when something in the TypeSpec schema is not directly compatible with GraphQL and the means of making it compatible are not deterministic.
7474

7575
## Output Types
7676

@@ -261,7 +261,7 @@ To emit a valid GraphQL and still represent the schema defined in TypeSpec, the
261261
- If the input type is a `Model` and all the properties of the `Model` are of valid Input types, a new `Input` object will be created in GraphQL, with the typename as the original type \+ `Input` suffix.
262262
- **🔴 Design decision:** All models are created with the `Input` suffix regardless of whether or not it is used as both, because the model can be used as both `input` and `output` in the future and changing the type name will cause issues with schema evolution.
263263
- **Cons:** the `Input` suffix can be annoying or result in types like `UserInputInput`
264-
- If the `model` or its properties are invalid Input types, the type of the invalid model or property will be assigned to the `Any` scalar type and a warning will be emitted.
264+
- If the `model` or its properties are invalid Input types, an error will be raised.
265265
- **🔴 Design decision:** In order to provide a different definition of the same field so that the GraphQL type can be represented more accurately, we will use the [upcoming visibility redesign to provide an alternative definition](https://discord.com/channels/1247582902930116749/1250119513681301514/1300865256679276655), see the examples to see what that could look like.
266266
- If the `model` contains an unbroken chain of non-null singular fields, throw an error and fail the emitter process
267267

@@ -1138,7 +1138,7 @@ enum DemoServicePersonSizeEnum {
11381138
### Design Alternatives
11391139

11401140
1. Use the type name instead of values for integer and floating point values. But, we would need to be consistent and use TSP enums in the type context rather than the value context which feels wrong.
1141-
2. Emit `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](https://discord.com/channels/1247582902930116749/1250119513681301514/1300865256679276655).
1141+
2. Emit `Any` for enums with values as integers or floating points and let the developer define an alternate type [using visibility](#visibility--never).
11421142
1. If the `@invisible` decorator can be applied to `EnumMembers`, 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:
11431143

11441144

@@ -1569,7 +1569,7 @@ type Query {
15691569
### Context and design challenges
15701570

15711571
* TypeSpec have two ways to filter out properties from Models:
1572-
* Visibility, using `@visibilty` and `@witthVisibility` decorators.
1572+
* Visibility, using `@visibilty`, `@invisible`, `@withVisibility`, et al decorators.
15731573
* `never` type
15741574
* The filtering based on explicit filtered models using `@withVisibility` is already considered in the compiler, so it will be also included in the emitter.
15751575
* HTTP library has the [automatic visibility](https://typespec.io/docs/libraries/http/operations/#automatic-visibility) concept that automatically filters the properties from the model based on the HTTP type of the operation, with no need of generating explicit filtered models.
@@ -1580,12 +1580,29 @@ type Query {
15801580
Add to the emitter the handling of the *`never`* type, and exclude any field from the Model before emitting the Model.
15811581
Note: This may result in empty models. We need to define what to do with fields pointing to empty Models.
15821582

1583-
For Implicit filtered models (automatic visibility):
1583+
Create a new [visibility class](https://typespec.io/docs/language-basics/visibility/#basic-concepts) named `OperationType`:
15841584

1585-
* Filter all output models using the "read" visibility, generating new models like ModelRead, or maybe ModelOutput. The new model would be generated only if it is distinct from the original Model.
1586-
* Since GraphQL does not distinguish between create, update and delete operations; we can generate our Input models just based on the GraphQL operations are used for: Query (visibility "query") or for Mutation (visibilities: "create", "update" and "delete"); generating: ModelQueryInput and ModelMutationInput.
1587-
* To emit a schema closer to those emitted by other emitters, if the operation is marked with a HTTP verb decorator, we will need to follow the HTTP library specification to filter the models before using them, and if needed, generate new models based on the visibility and the operation type. For example: for the operations responding using a Model, we will emit a new model named ModelRead with the properties filtered using the "read" visibility.
1588-
Note that the naming should include the Input suffix and this approach will generate models like UserCreateInput, UserUpdateInput, UserDeleteInput, etc.
1585+
```typespec
1586+
enum OperationType {
1587+
Query,
1588+
Mutation,
1589+
Subscription,
1590+
}
1591+
```
1592+
1593+
For implicit filtered models (automatic visibility):
1594+
1595+
GraphQL does not have an equivalent concept like HTTP verbs that map to the `Lifecycle` visibility modifiers. However, GraphQL mutations will commonly adhere to these type of "CRUD" operations.
1596+
1597+
TSP developers will need to take advantage of the [`@parameterVisibility`](https://typespec.io/docs/standard-library/built-in-decorators/#@parameterVisibility) and [`@returnTypeVisibility`](https://typespec.io/docs/standard-library/built-in-decorators/#@returnTypeVisibility) decorators to filter the models based on the semantic operation type.
1598+
In the case where the operation does not have explicit visibility specified and is already decorated with an HTTP verb, the emitter will use [the HTTP library specification](https://typespec.io/docs/libraries/http/operations/#automatic-visibility) to apply the related visibility to the input types.
1599+
1600+
If none of the standard "CRUD" operations apply, whether the [operation](#operations) is a query, mutation, or subscription will apply the `OperationType.Query`, `OperationType.Mutation`, or `OperationType.Subscription` visibility to input types, respectively.
1601+
1602+
For practical reasons, we will follow lead of the HTTP library on response types and filter them to `Lifecycle.Read` by default.
1603+
1604+
Generated model names will be suffixed with the appropriate operation type, e.g. `UserQueryInput`, `UserRead`, `UserCreateInput`, `UserMutationInput`, etc.
1605+
The new models would be generated only if they are distinct from the original Model.
15891606

15901607
### Examples
15911608
<table>
@@ -1599,18 +1616,18 @@ Note that the naming should include the Input suffix and this approach will gene
15991616
```typespec
16001617
/** Never and explicit filtering */
16011618
model PostBase<TState>; {
1602-
@visibility("read")
1619+
@visibility(Lifecycle.Read)
16031620
id: int32;
16041621
title: string;
16051622
isPopular: boolean;
1606-
@visibility("update")
1623+
@visibility(Lifecycle.Update)
16071624
poster?: Person;
16081625
postState: TState;
16091626
postCountry?: Country;
16101627
}
16111628
model Post is PostBase<int32>;
16121629
model PostGql is PostBase<never>;
1613-
@withVisibility("read")
1630+
@withVisibility(Lifecycle.Read)
16141631
model PostRead {
16151632
...Post;
16161633
}
@@ -1658,9 +1675,9 @@ type PostRead {
16581675
/** Automatic visibility with HTTP */
16591676
model User {
16601677
name: string;
1661-
@visibility("read", "update") id: string;
1662-
@visibility("create") password: string;
1663-
@visibility("read") lastPwdReset: plainDate;
1678+
@visibility(Lifecycle.Read, Lifecycle.Update) id: string;
1679+
@visibility(Lifecycle.Create) password: string;
1680+
@visibility(Lifecycle.Read) lastPwdReset: plainDate;
16641681
}
16651682
@route("/users")
16661683
interface Users {
@@ -1701,12 +1718,12 @@ type UserUpdateInput {
17011718
}
17021719

17031720
type Query {
1704-
get(id: String!): User!
1721+
get(id: String!): UserRead!
17051722
}
17061723

17071724
type Mutation {
1708-
create(user: UserCreateInput): User!
1709-
set(user: UserUpdateInput!): User!
1725+
create(user: UserCreateInput): UserRead!
1726+
set(user: UserUpdateInput!): UserRead!
17101727
}
17111728
```
17121729

@@ -1719,9 +1736,9 @@ type Mutation {
17191736
/** Automatic visibility with GraphQL */
17201737
model User {
17211738
name: string;
1722-
@visibility("read", "update") id: string;
1723-
@visibility("create") password: string;
1724-
@visibility("read") lastPwdReset: plainDate;
1739+
@visibility(Lifecycle.Read, Lifecycle.Update) id: string;
1740+
@visibility(Lifecycle.Create) password: string;
1741+
@visibility(Lifecycle.Read) lastPwdReset: plainDate;
17251742
}
17261743
interface Users {
17271744
@mutation create(user: User): User;
@@ -1756,12 +1773,12 @@ type UserMutationInput {
17561773
}
17571774

17581775
type Query {
1759-
get(id: String!): User!
1776+
get(id: String!): UserRead!
17601777
}
17611778

17621779
type Mutation {
1763-
create(user: UserCreateInput): User!
1764-
set(user: UserUpdateInput!): User!
1780+
create(user: UserCreateInput): UserRead!
1781+
set(user: UserUpdateInput!): UserRead!
17651782
}
17661783
```
17671784

@@ -1773,6 +1790,7 @@ type Mutation {
17731790

17741791
* Define what to do with fields pointing to empty models
17751792
* Should we keep the original Models in the schema, even if they are not used?
1793+
* We should expect that `<Model>Read` types will be the most common; should we have the `Lifecycle.Read`-filtered model instead be called `<Model>`, and the unfiltered model be something like `<Model>Full`?
17761794

17771795
## User feedback:
17781796

0 commit comments

Comments
 (0)