Skip to content

Commit 5203cbc

Browse files
committed
Add GraphQL example
1 parent 83356b3 commit 5203cbc

File tree

1 file changed

+304
-0
lines changed

1 file changed

+304
-0
lines changed

packages/graphql/letter-to-santa/optionality.md

+304
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,301 @@ components:
907907

908908
</details>
909909

910+
## "TeamMember" in Buildkite GraphQL API
911+
912+
The [Buildkite GraphQL API][buildkite-graphql-api] contains a few ways of interacting with team members that we'll look at below.
913+
914+
First, the definition of a [`TeamMember`][TeamMember]:
915+
916+
<details open><summary><code>TeamMember</code></summary>
917+
918+
```graphql
919+
"""An member of a team"""
920+
type TeamMember implements Node {
921+
"""The time when the team member was added"""
922+
createdAt: DateTime!
923+
924+
"""The user that added this team member"""
925+
createdBy: User
926+
id: ID!
927+
928+
"""The organization member associated with this team member"""
929+
organizationMember: OrganizationMember
930+
permissions: TeamMemberPermissions!
931+
932+
"""The users role within the team"""
933+
role: TeamMemberRole!
934+
935+
"""The team associated with this team member"""
936+
team: Team
937+
938+
"""The user associated with this team member"""
939+
user: User
940+
941+
"""The public UUID for this team member"""
942+
uuid: ID!
943+
}
944+
```
945+
</details>
946+
947+
The queries and mutations that deal with `TeamMember`s follow a fairly predictable pattern:
948+
[`teamMemberCreate()`][teamMemberCreate], [`teamMemberDelete()`][teamMemberDelete], [`teamMemberUpdate()`][teamMemberUpdate], and appearing as a connection on the `Team` object.
949+
950+
<details><summary>See the full query and mutation definitions</summary>
951+
952+
```graphql
953+
"""The query root for this schema"""
954+
type Query {
955+
"""Find a team"""
956+
team(
957+
"""The slug of the team, prefixed with its organization. i.e. `acme-inc/awesome-team`"""
958+
slug: ID!
959+
): Team
960+
}
961+
962+
"""The root for mutations in this schema"""
963+
type Mutation {
964+
"""Add a user to a team."""
965+
teamMemberCreate(
966+
"""Parameters for TeamMemberCreate"""
967+
input: TeamMemberCreateInput!
968+
): TeamMemberCreatePayload
969+
970+
"""Remove a user from a team."""
971+
teamMemberDelete(
972+
"""Parameters for TeamMemberDelete"""
973+
input: TeamMemberDeleteInput!
974+
): TeamMemberDeletePayload
975+
976+
"""Update a user's role in a team."""
977+
teamMemberUpdate(
978+
"""Parameters for TeamMemberUpdate"""
979+
input: TeamMemberUpdateInput!
980+
): TeamMemberUpdatePayload
981+
}
982+
```
983+
</details>
984+
985+
and they each define their own GraphQL input and object types for input and payload:
986+
[`TeamMemberCreateInput`][TeamMemberCreateInput], [`TeamMemberUpdateInput`][TeamMemberUpdateInput], [`TeamMemberDeleteInput`][TeamMemberDeleteInput]
987+
988+
<details><summary>Input and Payload types</summary>
989+
990+
```graphql
991+
input TeamMemberCreateInput {
992+
teamID: ID!
993+
userID: ID!
994+
995+
"""If no role is specified, the team member will be assigned the team's default role."""
996+
role: TeamMemberRole
997+
}
998+
999+
input TeamMemberUpdateInput {
1000+
id: ID!
1001+
role: TeamMemberRole!
1002+
}
1003+
1004+
input TeamMemberDeleteInput {
1005+
id: ID!
1006+
}
1007+
```
1008+
</details>
1009+
1010+
<details><summary>See the definition of <code>Team</code></summary>
1011+
1012+
```graphql
1013+
"""An organization team"""
1014+
type Team implements Node {
1015+
"""The time when this team was created"""
1016+
createdAt: DateTime!
1017+
1018+
"""The user that created this team"""
1019+
createdBy: User
1020+
1021+
"""New organization members will be granted this role on this team"""
1022+
defaultMemberRole: TeamMemberRole!
1023+
1024+
"""A description of the team"""
1025+
description: String
1026+
id: ID!
1027+
1028+
"""Add new organization members to this team by default"""
1029+
isDefaultTeam: Boolean!
1030+
1031+
"""Users that are part of this team"""
1032+
members(
1033+
first: Int
1034+
after: String
1035+
last: Int
1036+
before: String
1037+
1038+
"""Search team members named like the given query case insensitively"""
1039+
search: String
1040+
1041+
"""Search team members by their role"""
1042+
role: [TeamMemberRole!]
1043+
1044+
"""Order the members returned"""
1045+
order: TeamMemberOrder = RECENTLY_CREATED
1046+
): TeamMemberConnection
1047+
1048+
"""Whether or not team members can create new pipelines in this team"""
1049+
membersCanCreatePipelines: Boolean!
1050+
1051+
"""Whether or not team members can delete pipelines in this team"""
1052+
membersCanDeletePipelines: Boolean! @deprecated(reason: "This property has been removed without replacement")
1053+
1054+
"""The name of the team"""
1055+
name: String!
1056+
1057+
"""The organization that this team is a part of"""
1058+
organization: Organization
1059+
permissions: TeamPermissions!
1060+
1061+
"""Pipelines associated with this team"""
1062+
pipelines(
1063+
first: Int
1064+
after: String
1065+
last: Int
1066+
before: String
1067+
1068+
"""Search pipelines named like the given query case insensitively"""
1069+
search: String
1070+
1071+
"""Order the pipelines returned"""
1072+
order: TeamPipelineOrder = RECENTLY_CREATED
1073+
): TeamPipelineConnection
1074+
1075+
"""The privacy setting for this team"""
1076+
privacy: TeamPrivacy!
1077+
1078+
"""Registries associated with this team"""
1079+
registries(
1080+
first: Int
1081+
after: String
1082+
last: Int
1083+
before: String
1084+
1085+
"""Order the registries returned"""
1086+
order: TeamRegistryOrder = RECENTLY_CREATED
1087+
): TeamRegistryConnection
1088+
1089+
"""The slug of the team"""
1090+
slug: String!
1091+
1092+
"""Suites associated with this team"""
1093+
suites(
1094+
first: Int
1095+
after: String
1096+
last: Int
1097+
before: String
1098+
1099+
"""Order the suites returned"""
1100+
order: TeamSuiteOrder = RECENTLY_CREATED
1101+
): TeamSuiteConnection
1102+
1103+
"""The public UUID for this team"""
1104+
uuid: ID!
1105+
}
1106+
```
1107+
1108+
</details>
1109+
1110+
Here's how we might try to implement this in TypeSpec today:
1111+
(using concepts from the [GraphQL Emitter Design proposal][graphql-emitter-design])
1112+
1113+
<details open><summary>Current TypeSpec: ideally</summary>
1114+
1115+
```typespec
1116+
model TeamMember {
1117+
@visibility(Lifecycle.Read) createdAt: utcDateTime;
1118+
@visibility(Lifecycle.Read) createdBy?: User;
1119+
@visibility(Lifecycle.Delete, Lifecycle.Read) id: string;
1120+
@visibility(Lifecycle.Read) organizationMember?: OrganizationMember;
1121+
@visibility(Lifecycle.Read) permissions: TeamMemberPermissions;
1122+
@visibility(Lifecycle.Create, Lifecycle.Read, Lifecycle.Update) role: TeamMemberRole;
1123+
@visibility(Lifecycle.Read) team?: Team;
1124+
@visibility(Lifecycle.Create) team_id!: string;
1125+
@visibility(Lifecycle.Read) user? User;
1126+
@visibility(Lifecycle.Create) user_id!: string;
1127+
@visibility(Lifecycle.Read) uuid!: string;
1128+
}
1129+
1130+
@operationFields(members)
1131+
model Team {
1132+
id: string;
1133+
...
1134+
}
1135+
1136+
model TeamSearch {
1137+
search?: string;
1138+
role: TeamMemberRole[];
1139+
order: TeamMemberOrder = RECENTLY_CREATED;
1140+
}
1141+
1142+
@connection op members(...TeamSearch): TeamMember[];
1143+
1144+
@parameterVisibility(Lifecycle.Create)
1145+
@mutation teamMemberCreate(input: TeamMember): TeamMember;
1146+
1147+
@parameterVisibility(Lifecycle.Update)
1148+
@mutation teamMemberUpdate(input: TeamMember): TeamMember;
1149+
1150+
@parameterVisibility(Lifecycle.Delete)
1151+
@mutation teamMemberDelete(input: TeamMember): TeamMember;
1152+
```
1153+
1154+
</details>
1155+
1156+
However, there's a problem — `role` should be optional on create, but required on update and read.
1157+
The TypeSpec above will make it required everywhere.
1158+
To get around this we need to define a separate model:
1159+
1160+
```typespec
1161+
1162+
@withVisibility(Lifecycle.Create)
1163+
model TeamMemberCreate {
1164+
...TeamMember;
1165+
role?: TeamMemberRole;
1166+
}
1167+
```
1168+
1169+
but… we can't even do this because TypeScript will complain about the duplicate `role` property.
1170+
Instead, we would need to copy all the properties, or copy all the properties except `role` and define it separately in a `TeamMemberRead` model.
1171+
1172+
All of this seems confusing and somewhat arbitrary. For instance, should I create `TeamMemberUpdate` and `TeamMemberDelete` models, instead or in addition?
1173+
Which properties need to go on which models?
1174+
1175+
We'd like to describe the type system of our API, but instead we're creating arbitrary types to work around limitations in expressiveness.
1176+
1177+
With this proposal, we could instead do:
1178+
1179+
<details open><summary>Proposed TypeSpec</summary>
1180+
1181+
```typespec
1182+
model TeamMember {
1183+
@visibility(Lifecycle.Read) createdAt!: utcDateTime;
1184+
@visibility(Lifecycle.Read) createdBy?: User;
1185+
@visibility(Lifecycle.Delete, Lifecycle.Read) id!: string;
1186+
@visibility(Lifecycle.Read) organizationMember?: OrganizationMember;
1187+
@visibility(Lifecycle.Read) permissions!: TeamMemberPermissions;
1188+
1189+
@optional(Lifecycle.Create)
1190+
@visibility(Lifecycle.Create, Lifecycle.Read, Lifecycle.Update)
1191+
role!: TeamMemberRole;
1192+
1193+
@visibility(Lifecycle.Read) team?: Team;
1194+
@visibility(Lifecycle.Create) team_id: string;
1195+
@visibility(Lifecycle.Read) user? User;
1196+
@visibility(Lifecycle.Create) user_id: string;
1197+
@visibility(Lifecycle.Read) uuid: string;
1198+
}
1199+
```
1200+
1201+
</details>
1202+
1203+
without breaking up the model.
1204+
9101205
# Alternatives Considered
9111206

9121207
## decorator instead of `!`
@@ -1179,3 +1474,12 @@ gives us additional opportunities to flag code smells, which might include
11791474
[pinterest-customer-list]: https://developers.pinterest.com/docs/api/v5/customer_lists-update
11801475
[usage-decorator]: https://github.com/microsoft/typespec/issues/4486
11811476
[rest-parameter]: https://typespec.io/docs/extending-typespec/create-decorators/#rest-parameters
1477+
[graphql-emitter-design]: https://github.com/microsoft/typespec/issues/4933
1478+
[TeamMember]: https://buildkite.com/docs/apis/graphql/schemas/object/teammember
1479+
[TeamMemberCreateInput]: https://buildkite.com/docs/apis/graphql/schemas/input-object/teammembercreateinput
1480+
[TeamMemberUpdateInput]: https://buildkite.com/docs/apis/graphql/schemas/input-object/teammemberupdateinput
1481+
[TeamMemberDeleteInput]: https://buildkite.com/docs/apis/graphql/schemas/input-object/teammemberdeleteinput
1482+
[teamMemberCreate]: https://buildkite.com/docs/apis/graphql/schemas/mutation/teammembercreate
1483+
[teamMemberUpdate]: https://buildkite.com/docs/apis/graphql/schemas/mutation/teammemberupdate
1484+
[teamMemberDelete]: https://buildkite.com/docs/apis/graphql/schemas/mutation/teammemberdelete
1485+
[buildkite-graphql-api]: https://buildkite.com/docs/apis/graphql-api

0 commit comments

Comments
 (0)