├── .gitignore ├── LICENSE ├── README.md └── keystone-sdl.graphql /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | .idea 4 | .vscode 5 | 6 | node_modules 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Thinkmill Pty Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Thinkmill GraphQL Style Guide 2 | 3 | This style guide documents the standards we have developed for designing GraphQL Schemas at Thinkmill. 4 | 5 | - Concepts 6 | - Workflow and Process (GraphQL as Design System equivalent) 7 | - https://twitter.com/JedWatson/status/1170867029659791366 8 | - GraphQL as ideal abstraction layer for the business schema 9 | - Types 10 | - Schema Overview 11 | - See [Schemas and Types](https://graphql.org/learn/schema/) in the GraphQL Spec 12 | - Field type conventions and use-cases 13 | - ID (Scalar) 14 | - String (Scalar) 15 | - Boolean (Scalar) 16 | - Number (Int or Float) 17 | - Enum (Not quite a scalar type) 18 | - Date (Not built in) 19 | - Relationships 20 | - Queries 21 | - Standard Arguments 22 | - Filtering 23 | - Pagination 24 | - Sorting 25 | - Recursion of patterns/Nest Queries(?) 26 | - Mutations 27 | - Custom Types/Queries/Mutations (non-CRUD) 28 | - Authentication 29 | - Validation and Error Handling 30 | - Caching 31 | - Permissions 32 | 33 | ## Concepts 34 | 35 | In order to keep the style guide implementation-agnostic, we refer to _entities_, _entity types_ and _entity collections_. 36 | 37 | In an SQL-backed implementation, the _entity type_ for a `User` would be the schema; the _entity collection_ would be the `users` table and an `entity` would be a record in the table. 38 | 39 | Each _entity type_ has a singular and plural label, which we refer to as `{Entity}` and `{Entities}` in this style guide. Each entity also contains _fields_, which map to properties of the entity. 40 | 41 | We call entity properties _fields_, which have _field types_. Field types map to other entity types or scalar GraphQL Types. 42 | 43 | Throughout the guide, we will use the following example schema: 44 | 45 | ``` 46 | scalar DateTime 47 | 48 | enum PriorityEnum { 49 | LOW 50 | MEDIUM 51 | HIGH 52 | } 53 | 54 | type Todo { 55 | id: ID! 56 | name: String 57 | priority: PriorityEnum 58 | dueDate: DateTime 59 | user: User 60 | } 61 | 62 | type User { 63 | id: ID! 64 | name: String 65 | age: Int 66 | height: Float 67 | todos: [Todo] 68 | } 69 | 70 | Query { 71 | # Get one todo item 72 | Todo(id: ID!): Todo 73 | # Get all todo items 74 | allTodos: [Todo!]! 75 | } 76 | 77 | Mutation { 78 | addTodo(name: String!, priority: Priority = LOW): Todo! 79 | removeTodo(id: ID!): Todo! 80 | } 81 | ``` 82 | 83 | ```gql 84 | query { 85 | allTodos { 86 | name 87 | users(first: 10) { 88 | name 89 | } 90 | } 91 | } 92 | ``` 93 | 94 | ## Workflow and Process 95 | 96 | // TODO 97 | 98 | - How do you come up with a schema? 99 | - System requirements -> schema 100 | - Identify the queries/mutations you need and use this to inform the data schema 101 | - How do we get UX designers to think about the queries they need? 102 | - A graphQL schema can be easily related across the development spectrum 103 | - Given a UX design, how do you identify the schema requirements (in the same way you'd identify the design system requirements) 104 | 105 | ## Queries 106 | 107 | For each entity type we generate the four top level queries: 108 | 109 | - `all{Entities}` 110 | - `{Entity}` 111 | - `_all{Entities}Meta` 112 | - `_{Entity}Meta` 113 | 114 | #### `{Entity}` 115 | 116 | ```gql 117 | query { 118 | User(where: { id: ID }) { 119 | name 120 | } 121 | } 122 | ``` 123 | 124 | Retrieves an entity from a collection. The single entity query has one argument. 125 | 126 | #### `all{Entities}` 127 | 128 | ```gql 129 | query { 130 | allUsers { 131 | id 132 | } 133 | } 134 | ``` 135 | 136 | Retrieves all entities from a collection. 137 | 138 | #### `_all{Entities}Meta` 139 | 140 | ```gql 141 | query { 142 | _allUsersMeta { 143 | count 144 | } 145 | } 146 | ``` 147 | 148 | Get the total number of entities from a collection. 149 | 150 | #### `_{Entity}Meta` 151 | 152 | // TODO: This should probably be taken out of the spec 153 | 154 | ```gql 155 | query { 156 | _UserMeta(where: { id: ID }) { 157 | count 158 | } 159 | } 160 | ``` 161 | 162 | ### Filtering 163 | 164 | These apply to queries for the `all{Entities}` and `{all}EntitiesMeta` types 165 | 166 | #### `where` 167 | 168 | ```gql 169 | query { 170 | allUsers (where: { id_starts_with_i: 'A'} ) { 171 | id 172 | } 173 | } 174 | ``` 175 | 176 | Limit results to those matching the where clause. Where clauses generated will depend on the field types available. 177 | 178 | ##### Operators 179 | 180 | // TODO: Where does this section belong? 181 | 182 | - `AND`: [UserWhereInput] 183 | - `OR`: [UserWhereInput] 184 | 185 | #### `search` 186 | 187 | > Need to consider whether this should be included or whether it's just a KeystoneJS specific thing 188 | 189 | Will search the entity collection to limit results. Search logic is defined by the entity type. 190 | 191 | ```gql 192 | query { 193 | allUsers(search: "Mike") { 194 | id 195 | } 196 | } 197 | ``` 198 | 199 | ### Sorting 200 | 201 | #### `orderBy` 202 | 203 | ```gql 204 | query { 205 | allUsers(orderBy: "name_ASC") { 206 | id 207 | } 208 | } 209 | ``` 210 | 211 | > Ascending vs Descending? 212 | 213 | Will order the results. The orderBy string should match a field name in the entity collection. 214 | 215 | ### Pagination 216 | 217 | #### `first` 218 | 219 | ```gql 220 | query { 221 | allUsers(first: 10) { 222 | id 223 | } 224 | } 225 | ``` 226 | 227 | Select this many results from the entity collection, sorted by the `orderBy` argument and matching the `where` and `search` values. Works like specifying `TOP` in SQL. 228 | 229 | If less results are available, the number of available results will be returned. 230 | 231 | #### `skip` 232 | 233 | ```gql 234 | query { 235 | allUsers(skip: 10) { 236 | id 237 | } 238 | } 239 | ``` 240 | 241 | Skip the number of results specified, before returning the number of results specified by the `first` argument, sorted by the `orderBy` argument and matching the `where` and `search` values. 242 | 243 | If the value of `skip` is greater than the number of available results, zero results will be returned. 244 | 245 | ### Combining query arguments for pagination 246 | 247 | When `first` and `skip` are used together with the `count` from `_all{Entities}Meta`, this is sufficient to implement pagination of an entity collection. 248 | 249 | It is important to provide the same `where` and `search` arguments to both the `all{Entities}` and `_all{Entities}Meta` queries. For example: 250 | 251 | ```gql 252 | query { 253 | allUsers (search:'a', skip: 10, first: 10) { 254 | id 255 | } 256 | _allUsersMeta(search: 'a') { 257 | count 258 | } 259 | } 260 | 261 | ``` 262 | 263 | When `first` and `skip` are used together, skip works as an offset for the `first` argument. For example`(skip:10, first:10)` selects results 11 through 20. 264 | 265 | Both `skip` and `first` respect the values of the `where`, `search` and `orderBy` arguments. 266 | 267 | ## Field types 268 | 269 | ### ID 270 | 271 | - `{Field}`: ID 272 | - `{Field}_not`: ID 273 | - `{Field}_in`: [ID!] 274 | - `{Field}_not_in`: [ID!] 275 | 276 | ### String 277 | 278 | - `{Field}:` String 279 | - `{Field}_not`: String 280 | - `{Field}_contains`: String 281 | - `{Field}_not_contains`: String 282 | - `{Field}_starts_with`: String 283 | - `{Field}_not_starts_with`: String 284 | - `{Field}_ends_with`: String 285 | - `{Field}_not_ends_with`: String 286 | - `{Field}_i`: String 287 | - `{Field}_not_i`: String 288 | - `{Field}_contains_i`: String 289 | - `{Field}_not_contains_i`: String 290 | - `{Field}_starts_with_i`: String 291 | - `{Field}_not_starts_with_i`: String 292 | - `{Field}_ends_with_i`: String 293 | - `{Field}_not_ends_with_i`: String 294 | - `{Field}_in`: [String] 295 | - `{Field}_not_in`: [String] 296 | 297 | ### Number 298 | 299 | - `{Field}: Int` 300 | - `{Field}_not`: Int 301 | - `{Field}_lt`: Int 302 | - `{Field}_lte`: Int 303 | - `{Field}_gt`: Int 304 | - `{Field}_gte`: Int 305 | - `{Field}_in`: [Int] 306 | - `{Field}_not_in`: [Int] 307 | 308 | ### Boolean 309 | 310 | // TODO 311 | 312 | ### Enum 313 | 314 | // TODO 315 | 316 | ### Date 317 | 318 | // TODO 319 | 320 | ## Mutations 321 | 322 | // TODO 323 | -------------------------------------------------------------------------------- /keystone-sdl.graphql: -------------------------------------------------------------------------------- 1 | ## A keystone generated schema for the example defined in readme 2 | 3 | directive @cacheControl( 4 | maxAge: Int 5 | scope: CacheControlScope 6 | ) on FIELD_DEFINITION | OBJECT | INTERFACE 7 | type _ListAccess { 8 | # Access Control settings for the currently logged in (or anonymous) 9 | # user when performing 'create' operations. 10 | # NOTE: 'create' can only return a Boolean. 11 | # It is not possible to specify a declarative Where clause for this 12 | # operation 13 | create: Boolean 14 | # Access Control settings for the currently logged in (or anonymous) 15 | # user when performing 'read' operations. 16 | read: JSON 17 | # Access Control settings for the currently logged in (or anonymous) 18 | # user when performing 'update' operations. 19 | update: JSON 20 | # Access Control settings for the currently logged in (or anonymous) 21 | # user when performing 'delete' operations. 22 | delete: JSON 23 | } 24 | 25 | type _ListMeta { 26 | # The Keystone List name 27 | name: String 28 | # Access control configuration for the currently authenticated 29 | # request 30 | access: _ListAccess 31 | # Information on the generated GraphQL schema 32 | schema: _ListSchema 33 | } 34 | 35 | type _ListSchema { 36 | # The typename as used in GraphQL queries 37 | type: String 38 | # Top level GraphQL query names which either return this type, or 39 | # provide aggregate information about this type 40 | queries: [String] 41 | # Information about fields on other types which return this type, or 42 | # provide aggregate information about this type 43 | relatedFields: [_ListSchemaRelatedFields] 44 | } 45 | 46 | type _ListSchemaRelatedFields { 47 | # The typename as used in GraphQL queries 48 | type: String 49 | # A list of GraphQL field names 50 | fields: [String] 51 | } 52 | 53 | type _QueryMeta { 54 | count: Int 55 | } 56 | 57 | enum CacheControlScope { 58 | PUBLIC 59 | PRIVATE 60 | } 61 | 62 | input CompaniesCreateInput { 63 | data: CompanyCreateInput 64 | } 65 | 66 | input CompaniesUpdateInput { 67 | id: ID! 68 | data: CompanyUpdateInput 69 | } 70 | 71 | # A keystone list 72 | type Company { 73 | # This virtual field will be resolved in one of the following ways (in this order): 74 | # 1. Execution of 'labelResolver' set on the Company List config, or 75 | # 2. As an alias to the field set on 'labelField' in the Company List config, or 76 | # 3. As an alias to a 'name' field on the Company List (if one exists), or 77 | # 4. As an alias to the 'id' field on the Company List. 78 | _label_: String 79 | id: ID 80 | name: String 81 | employeeCount: Int 82 | } 83 | 84 | input CompanyCreateInput { 85 | name: String 86 | employeeCount: Int 87 | } 88 | 89 | input CompanyRelateToOneInput { 90 | create: CompanyCreateInput 91 | connect: CompanyWhereUniqueInput 92 | disconnect: CompanyWhereUniqueInput 93 | disconnectAll: Boolean 94 | } 95 | 96 | input CompanyUpdateInput { 97 | name: String 98 | employeeCount: Int 99 | } 100 | 101 | input CompanyWhereInput { 102 | AND: [CompanyWhereInput] 103 | OR: [CompanyWhereInput] 104 | id: ID 105 | id_not: ID 106 | id_in: [ID] 107 | id_not_in: [ID] 108 | name: String 109 | name_not: String 110 | name_contains: String 111 | name_not_contains: String 112 | name_starts_with: String 113 | name_not_starts_with: String 114 | name_ends_with: String 115 | name_not_ends_with: String 116 | name_i: String 117 | name_not_i: String 118 | name_contains_i: String 119 | name_not_contains_i: String 120 | name_starts_with_i: String 121 | name_not_starts_with_i: String 122 | name_ends_with_i: String 123 | name_not_ends_with_i: String 124 | name_in: [String] 125 | name_not_in: [String] 126 | employeeCount: Int 127 | employeeCount_not: Int 128 | employeeCount_lt: Int 129 | employeeCount_lte: Int 130 | employeeCount_gt: Int 131 | employeeCount_gte: Int 132 | employeeCount_in: [Int] 133 | employeeCount_not_in: [Int] 134 | } 135 | 136 | input CompanyWhereUniqueInput { 137 | id: ID! 138 | } 139 | 140 | # DateTime custom scalar represents 141 | an ISO 8601 datetime string 142 | scalar DateTime 143 | 144 | # A keystone list 145 | type Email { 146 | # This virtual field will be resolved in one of the following ways (in this order): 147 | # 1. Execution of 'labelResolver' set on the Email List config, or 148 | # 2. As an alias to the field set on 'labelField' in the Email List config, or 149 | # 3. As an alias to a 'name' field on the Email List (if one exists), or 150 | # 4. As an alias to the 'id' field on the Email List. 151 | _label_: String 152 | id: ID 153 | email: String 154 | isVerified: Boolean 155 | } 156 | 157 | input EmailCreateInput { 158 | email: String 159 | isVerified: Boolean 160 | } 161 | 162 | input EmailRelateToManyInput { 163 | create: [EmailCreateInput] 164 | connect: [EmailWhereUniqueInput] 165 | disconnect: [EmailWhereUniqueInput] 166 | disconnectAll: Boolean 167 | } 168 | 169 | input EmailsCreateInput { 170 | data: EmailCreateInput 171 | } 172 | 173 | input EmailsUpdateInput { 174 | id: ID! 175 | data: EmailUpdateInput 176 | } 177 | 178 | input EmailUpdateInput { 179 | email: String 180 | isVerified: Boolean 181 | } 182 | 183 | input EmailWhereInput { 184 | AND: [EmailWhereInput] 185 | OR: [EmailWhereInput] 186 | id: ID 187 | id_not: ID 188 | id_in: [ID] 189 | id_not_in: [ID] 190 | email: String 191 | email_not: String 192 | email_contains: String 193 | email_not_contains: String 194 | email_starts_with: String 195 | email_not_starts_with: String 196 | email_ends_with: String 197 | email_not_ends_with: String 198 | email_i: String 199 | email_not_i: String 200 | email_contains_i: String 201 | email_not_contains_i: String 202 | email_starts_with_i: String 203 | email_not_starts_with_i: String 204 | email_ends_with_i: String 205 | email_not_ends_with_i: String 206 | email_in: [String] 207 | email_not_in: [String] 208 | isVerified: Boolean 209 | isVerified_not: Boolean 210 | } 211 | 212 | input EmailWhereUniqueInput { 213 | id: ID! 214 | } 215 | 216 | # The `JSON` scalar type 217 | represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). 218 | scalar JSON 219 | 220 | type Mutation { 221 | # Create a single User item. 222 | createUser(data: UserCreateInput): User 223 | # Create multiple User items. 224 | createUsers(data: [UsersCreateInput]): [User] 225 | # Update a single User item by ID. 226 | updateUser(id: ID!, data: UserUpdateInput): User 227 | # Update multiple User items by ID. 228 | updateUsers(data: [UsersUpdateInput]): [User] 229 | # Delete a single User item by ID. 230 | deleteUser(id: ID!): User 231 | # Delete multiple User items by ID. 232 | deleteUsers(ids: [ID!]): [User] 233 | # Create a single Company item. 234 | createCompany(data: CompanyCreateInput): Company 235 | # Create multiple Company items. 236 | createCompanies(data: [CompaniesCreateInput]): [Company] 237 | # Update a single Company item by ID. 238 | updateCompany(id: ID!, data: CompanyUpdateInput): Company 239 | # Update multiple Company items by ID. 240 | updateCompanies(data: [CompaniesUpdateInput]): [Company] 241 | # Delete a single Company item by ID. 242 | deleteCompany(id: ID!): Company 243 | # Delete multiple Company items by ID. 244 | deleteCompanies(ids: [ID!]): [Company] 245 | # Create a single Email item. 246 | createEmail(data: EmailCreateInput): Email 247 | # Create multiple Email items. 248 | createEmails(data: [EmailsCreateInput]): [Email] 249 | # Update a single Email item by ID. 250 | updateEmail(id: ID!, data: EmailUpdateInput): Email 251 | # Update multiple Email items by ID. 252 | updateEmails(data: [EmailsUpdateInput]): [Email] 253 | # Delete a single Email item by ID. 254 | deleteEmail(id: ID!): Email 255 | # Delete multiple Email items by ID. 256 | deleteEmails(ids: [ID!]): [Email] 257 | } 258 | 259 | type Query { 260 | # Search for all User items which match the where clause. 261 | allUsers( 262 | where: UserWhereInput 263 | search: String 264 | orderBy: String 265 | first: Int 266 | skip: Int 267 | ): [User] 268 | # Search for the User item with the matching ID. 269 | User(where: UserWhereUniqueInput!): User 270 | # Perform a meta-query on all User items which match the where clause. 271 | _allUsersMeta( 272 | where: UserWhereInput 273 | search: String 274 | orderBy: String 275 | first: Int 276 | skip: Int 277 | ): _QueryMeta 278 | # Retrieve the meta-data for the User list. 279 | _UsersMeta: _ListMeta 280 | # Search for all Company items which match the where clause. 281 | allCompanies( 282 | where: CompanyWhereInput 283 | search: String 284 | orderBy: String 285 | first: Int 286 | skip: Int 287 | ): [Company] 288 | # Search for the Company item with the matching ID. 289 | Company(where: CompanyWhereUniqueInput!): Company 290 | # Perform a meta-query on all Company items which match the where clause. 291 | _allCompaniesMeta( 292 | where: CompanyWhereInput 293 | search: String 294 | orderBy: String 295 | first: Int 296 | skip: Int 297 | ): _QueryMeta 298 | # Retrieve the meta-data for the Company list. 299 | _CompaniesMeta: _ListMeta 300 | # Search for all Email items which match the where clause. 301 | allEmails( 302 | where: EmailWhereInput 303 | search: String 304 | orderBy: String 305 | first: Int 306 | skip: Int 307 | ): [Email] 308 | # Search for the Email item with the matching ID. 309 | Email(where: EmailWhereUniqueInput!): Email 310 | # Perform a meta-query on all Email items which match the where clause. 311 | _allEmailsMeta( 312 | where: EmailWhereInput 313 | search: String 314 | orderBy: String 315 | first: Int 316 | skip: Int 317 | ): _QueryMeta 318 | # Retrieve the meta-data for the Email list. 319 | _EmailsMeta: _ListMeta 320 | # Retrieve the meta-data for all lists. 321 | _ksListsMeta: [_ListMeta] 322 | } 323 | 324 | # The `Upload` scalar type 325 | represents a file upload. 326 | scalar Upload 327 | 328 | # A keystone list 329 | type User { 330 | # This virtual field will be resolved in one of the following ways (in this order): 331 | # 1. Execution of 'labelResolver' set on the User List config, or 332 | # 2. As an alias to the field set on 'labelField' in the User List config, or 333 | # 3. As an alias to a 'name' field on the User List (if one exists), or 334 | # 4. As an alias to the 'id' field on the User List. 335 | _label_: String 336 | id: ID 337 | name: String 338 | company: Company 339 | dob: DateTime 340 | status: Int 341 | emails( 342 | where: EmailWhereInput 343 | search: String 344 | orderBy: String 345 | first: Int 346 | skip: Int 347 | ): [Email] 348 | _emailsMeta( 349 | where: EmailWhereInput 350 | search: String 351 | orderBy: String 352 | first: Int 353 | skip: Int 354 | ): _QueryMeta 355 | } 356 | 357 | input UserCreateInput { 358 | name: String 359 | company: CompanyRelateToOneInput 360 | dob: DateTime 361 | status: Int 362 | emails: EmailRelateToManyInput 363 | } 364 | 365 | input UsersCreateInput { 366 | data: UserCreateInput 367 | } 368 | 369 | input UsersUpdateInput { 370 | id: ID! 371 | data: UserUpdateInput 372 | } 373 | 374 | input UserUpdateInput { 375 | name: String 376 | company: CompanyRelateToOneInput 377 | dob: DateTime 378 | status: Int 379 | emails: EmailRelateToManyInput 380 | } 381 | 382 | input UserWhereInput { 383 | AND: [UserWhereInput] 384 | OR: [UserWhereInput] 385 | id: ID 386 | id_not: ID 387 | id_in: [ID] 388 | id_not_in: [ID] 389 | name: String 390 | name_not: String 391 | name_contains: String 392 | name_not_contains: String 393 | name_starts_with: String 394 | name_not_starts_with: String 395 | name_ends_with: String 396 | name_not_ends_with: String 397 | name_i: String 398 | name_not_i: String 399 | name_contains_i: String 400 | name_not_contains_i: String 401 | name_starts_with_i: String 402 | name_not_starts_with_i: String 403 | name_ends_with_i: String 404 | name_not_ends_with_i: String 405 | name_in: [String] 406 | name_not_in: [String] 407 | company: CompanyWhereInput 408 | company_is_null: Boolean 409 | dob: DateTime 410 | dob_not: DateTime 411 | dob_lt: DateTime 412 | dob_lte: DateTime 413 | dob_gt: DateTime 414 | dob_gte: DateTime 415 | dob_in: [DateTime] 416 | dob_not_in: [DateTime] 417 | status: Int 418 | status_not: Int 419 | status_lt: Int 420 | status_lte: Int 421 | status_gt: Int 422 | status_gte: Int 423 | status_in: [Int] 424 | status_not_in: [Int] 425 | # condition must be true for all nodes 426 | emails_every: EmailWhereInput 427 | # condition must be true for at least 1 node 428 | emails_some: EmailWhereInput 429 | # condition must be false for all nodes 430 | emails_none: EmailWhereInput 431 | # is the relation field null 432 | emails_is_null: Boolean 433 | } 434 | 435 | input UserWhereUniqueInput { 436 | id: ID! 437 | } 438 | 439 | --------------------------------------------------------------------------------