├── README.md ├── docs └── index.html └── gds-spec.md /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL Data Specification 2 | 3 | The specification is written in `gds-spec.md` and uses [spec-md](https://spec-md.com). 4 | 5 | ## Build & preview 6 | 7 | ```bash 8 | npm i -g spec-md http-server 9 | 10 | # build 11 | spec-md gds-spec.md > html/index.html 12 | 13 | # preview 14 | cd html 15 | http-server . 16 | 17 | # Open browser at localhost:8080 18 | ``` 19 | 20 | ## Deploy 21 | 22 | The HTML in the html/ directory is automatically deployed via GitHub pages. 23 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GraphQL Data Specification 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |

GraphQL Data Specification

14 |
15 |

This document lays out GraphQL schema design principles & standards for building data APIs.

16 |

This is ideal for GraphQL or federated GraphQL schemas that need to:

17 |
    18 |
  1. Provide a standardized API
  2. 19 |
  3. Provide a composable and fleixble design
  4. 20 |
  5. Balance domain-driven and consumer-driven design
  6. 21 |
  7. Support lightweight and high-velocity iterations on schema design
  8. 22 |
23 |
24 | 85 |
86 |
87 |

1Schema design principles

88 |

When building a GraphQL schema, the schema design process should follow the following principles:

89 |
    90 |
  1. The GraphQL schema design process allows multiple stakeholders to own parts of the schema.
  2. 91 |
  3. Domain owners design their part of the GraphQL schema in a way that accurately reflects the domain. These might require keeping in mind the needs of multiple API consumers domain driven design.
  4. 92 |
  5. Consumers should be able to drive independent additions to the GraphQL schema that represent consumer specific needs.
  6. 93 |
  7. Any entity in the GraphQL schema should either represent a resource, a unit of data, or a method, a unit of business logic.
  8. 94 |
95 |
96 |
97 |

2Objects

98 |

Every data object in the described domain, e.g., a User, an Order, or a Product in an e-commerce application, has a GraphQL type.

99 |
100 |

2.1Example

101 |
type User {
102 |   user_id: Int!
103 |   name: String
104 | }
105 | 
106 |
107 |
108 |
109 |

3Global ID

110 |

An id field in the object can represent a globally unique id for the object. This allows the Object type to implement the Relay Node interface.

111 |
112 |

3.1Example

113 |
type User implements Node {
114 |   id: ID!
115 |   user_id: Int!
116 |   name: String
117 | }
118 | 
119 |
120 |
121 |
122 |

4Selection (Models)

123 |

Models are collections of objects that can be queried in standardized ways and represent standardized selection, query or read operations.

124 |
125 |

4.1Single Object Selection

126 |

If one or more fields can uniquely identify an object in a model, a root field should allow querying the object using those fields.

127 |
128 |

4.1.1Example

129 |
type Query {
130 |   user_by_id(user_id: Int!): User
131 | }
132 | 
133 |
134 |
135 |
136 |

4.2Multi Object Selection

137 |

To fetch multiple objects, a root field should return a list of objects.

138 |
139 |

4.2.1Example

140 |
type Query {
141 |   users: [User]
142 | }
143 | 
144 |

By default semantics, this should return all the objects in the model. Additional input paramenters can control the set of objects returned by the model.

145 |
146 |
147 |
148 |
149 |

5Filtering

150 |

A where input parameter for a model allows querying a model based on some condition on one or more of its fields. This includes any relationship fields that have been defined on the object.

151 |

A particular field will support specific comparison operators. The where input paramenter should support operators for AND / OR / NOT to compose a filter expression.

152 |
153 |

5.1Example

154 |
type Query {
155 |   users(where: User_boolean_exp): [User]
156 | }
157 | 
158 | input User_boolean_exp {
159 |   _and: [User_boolean_exp!]
160 |   _or: [User_boolean_exp!]
161 |   _not: User_boolean_exp
162 |   user_id: Int_comparison_exp
163 |   first_name: String_comparison_exp
164 |   last_name: String_comparison_exp
165 |   email: String_comparison_exp
166 | }
167 | 
168 | input Int_comparison_exp {
169 |   _eq: Int
170 |   _lt: Int
171 |   _gt: Int
172 |   _lte: Int
173 |   _gte: Int
174 |   _in: [Int!]
175 | }
176 | 
177 | input String_comparison_exp {
178 |   _eq: String
179 |   _in: [Int!]
180 |   _like: String
181 | }
182 | 
183 |
184 |
185 |

5.2Filter expression grammar

186 | 189 | 195 |
196 | FieldComparisonExpression
FieldOperatorUserInputValue
197 |
198 |
199 | Field
ObjectField1
200 |
ObjectField2
201 |
ObjectField3
202 |
...
203 |
204 |
205 |
206 |
207 |

6Sorting

208 |

OpenDD can generate an input for sorting the objects when querying a model based on one or more of its fields. This includes any relationship fields that have been defined on the object. Multiple fields can be used as the sort key, with the order of specification determining which one gets applied first.

209 |
210 |

6.1Example

211 |
type Query {
212 |   users(where: [User_order_by_exp]): [User]
213 | }
214 | 
215 | input User_order_by_exp {
216 |   user_id: OrderByDirection
217 |   first_name: OrderByDirection
218 |   last_name: OrderByDirection
219 |   email: OrderByDirection
220 | }
221 | 
222 | enum OrderByDirection {
223 |   Asc,
224 |   Desc
225 | }
226 | 
227 |
228 |
229 |
230 |

7Pagination

231 |

OpenDD can generate input arguments for paginating through the objects returned when querying a model.

232 |
233 |

7.1Example

234 |
type Query {
235 |   users(limit: Int, offset: Int): [User]
236 | }
237 | 
238 |
239 |
240 |
241 |

8Aggregation

242 |

TODO: Add after v3 supports aggregation.

243 |
244 |
245 |

9Relay

246 |

The GraphQL API generated by OpenDD implements the Relay API and has a node query root field that can be used to retrieve specific objects using their global ID.

247 |
248 |

9.1Example

249 |
type Query {
250 |   node(id: ID!): Node
251 | }
252 | 
253 |
254 |
255 |
256 |

10Commands

257 |

Commands are functions which take in some arguments and produce an output. The semantics of commands except for arguments and output are opaque.

258 |

Commands can be made available for invocation either at the query root or the mutation root of the command GraphQL API.

259 |
260 |

10.1Example

261 |
type Mutation {
262 |   validateDiscountCoupon(coupon_code: String): bool
263 | }
264 | 
265 |
266 |
267 |
268 |

11Relationships

269 |

Objects can be augmented with related information by defining a relationship from the object to a model or a command. This way, queries can be flexibly composed.

270 |

Creating a relationship adds a relationship field to the generated object type which can be used to query the related model or command.

271 |
272 |

11.1Example

273 |
type Order {
274 |   order_id: Int!
275 |   product_id: Int!
276 |   user_id: Int!
277 |   user: User // relationship from Order.user_id to User.user_id
278 | }
279 | 
280 |
281 |
282 |
283 |

12Query Composition

284 |

For a has-many (array) relationship, the input parameters of multi-object model selection are available to use, so queries across relationships can be flexibly composed.

285 |
286 |

12.1Example

287 |

If there is a has-many relationship from User to orders and a has-one (object) relationship from Order to users:

288 |
type User {
289 |   user_id: Int!
290 |   name: String
291 |   orders(
292 |     where: Orders_bool_exp,
293 |     order_by: Orders_order_by_exp,
294 |     limit: Int,
295 |     offset: Int
296 |   ): [Order]
297 | }
298 | 
299 |

Because relationships, filtering, and sorting are semantically meaningful in OpenDD, this allows for efficiently issuing such composable queries without resorting to N+1 database queries, even when the two related models come from different databases.

300 |
301 |
302 |

12.2Example

303 |

The following query results in only 2 database lookups, one for users and one for orders, as opposed to 11 lookups if resolving traditionally. Such efficient composition is impossible to achieve with generic resolver-based approaches even when using data loaders.

304 |
query {
305 |   users(limit: 10) {
306 |     user_id
307 |     orders(where: { total: { _gt: 100 } }, order_by: { order_date: DESC }) {
308 |       order_id
309 |       product_id
310 |     }
311 |   }
312 | }
313 | 
314 |
315 |
316 |
317 |

13Filtering / Sorting Composition

318 |

Relationship fields themselves can be used for filtering, sorting, and aggregating a model containing the source object.

319 |
320 |

13.1Examples

321 |

To query all users who ordered a particular product where orders are not a part of the users model:

322 |
query {
323 |   users(where: { orders: { product_id: { _eq: "product_1" } } }) {
324 |     user_id
325 |   }
326 | }
327 | 
328 |

To sort all orders of a particular product by the email of the user that placed the order, where the user email is not a part of the orders model:

329 |
query {
330 |   orders(where: { product_id: { _eq: 1 } }, order_by: { user: { email: ASC } }) {
331 |     order_id
332 |   }
333 | }
334 | 
335 |

This is possible since OpenDD has a semantic understanding of relationships and predicates. Doing this would be impossible in generic resolver-based approaches.

336 |
337 |
338 |
339 |

340 | §Index

341 |
    342 |
  1. Field
  2. 343 |
  3. FieldComparisonExpression
  4. 344 |
  5. ModelBooleanExpression
  6. 345 |
  7. Where
  8. 346 |
347 |
348 |
349 | 351 | 352 |
353 |
354 | 355 |
  1. 1Schema design principles
  2. 356 |
  3. 2Objects 357 | 358 |
      359 |
    1. 2.1Example
    2. 360 |
    361 |
  4. 362 |
  5. 3Global ID 363 | 364 |
      365 |
    1. 3.1Example
    2. 366 |
    367 |
  6. 368 |
  7. 4Selection (Models) 369 | 370 |
      371 |
    1. 4.1Single Object Selection 372 | 373 |
        374 |
      1. 4.1.1Example
      2. 375 |
      376 |
    2. 377 |
    3. 4.2Multi Object Selection 378 | 379 |
        380 |
      1. 4.2.1Example
      2. 381 |
      382 |
    4. 383 |
    384 |
  8. 385 |
  9. 5Filtering 386 | 387 |
      388 |
    1. 5.1Example
    2. 389 |
    3. 5.2Filter expression grammar
    4. 390 |
    391 |
  10. 392 |
  11. 6Sorting 393 | 394 |
      395 |
    1. 6.1Example
    2. 396 |
    397 |
  12. 398 |
  13. 7Pagination 399 | 400 |
      401 |
    1. 7.1Example
    2. 402 |
    403 |
  14. 404 |
  15. 8Aggregation
  16. 405 |
  17. 9Relay 406 | 407 |
      408 |
    1. 9.1Example
    2. 409 |
    410 |
  18. 411 |
  19. 10Commands 412 | 413 |
      414 |
    1. 10.1Example
    2. 415 |
    416 |
  20. 417 |
  21. 11Relationships 418 | 419 |
      420 |
    1. 11.1Example
    2. 421 |
    422 |
  22. 423 |
  23. 12Query Composition 424 | 425 |
      426 |
    1. 12.1Example
    2. 427 |
    3. 12.2Example
    4. 428 |
    429 |
  24. 430 |
  25. 13Filtering / Sorting Composition 431 | 432 |
      433 |
    1. 13.1Examples
    2. 434 |
    435 |
  26. 436 |
  27. §Index
  28. 437 |
438 |
439 |
440 | 441 | 442 | -------------------------------------------------------------------------------- /gds-spec.md: -------------------------------------------------------------------------------- 1 | # GraphQL Data Specification 2 | 3 | This document lays out GraphQL schema design principles & standards for building data APIs. 4 | 5 | This is ideal for GraphQL or federated GraphQL schemas that need to: 6 | 7 | 1. Provide a standardized API 8 | 2. Provide a composable and fleixble design 9 | 3. Balance domain-driven and consumer-driven design 10 | 4. Support lightweight and high-velocity iterations on schema design 11 | 12 | ## Schema design principles 13 | 14 | When building a GraphQL schema, the schema design process should follow the following principles: 15 | 16 | 1. The GraphQL schema design process allows multiple stakeholders to own parts of the schema. 17 | 2. Domain owners design their part of the GraphQL schema in a way that accurately reflects the domain. These might require keeping in mind the needs of multiple API consumers domain driven design. 18 | 3. Consumers should be able to drive independent additions to the GraphQL schema that represent consumer specific needs. 19 | 4. Any entity in the GraphQL schema should either represent a resource, a unit of data, or a method, a unit of business logic. 20 | 21 | 22 | ## Objects 23 | 24 | Every data object in the described domain, e.g., a User, an Order, or a Product in an e-commerce application, has a GraphQL `type`. 25 | 26 | ### Example 27 | 28 | ```graphql 29 | type User { 30 | user_id: Int! 31 | name: String 32 | } 33 | ``` 34 | 35 | ## Global ID 36 | 37 | An `id` field in the object can represent a globally unique id for the object. This allows the Object type to implement the [Relay Node]() interface. 38 | 39 | ### Example 40 | 41 | ```graphql 42 | type User implements Node { 43 | id: ID! 44 | user_id: Int! 45 | name: String 46 | } 47 | ``` 48 | 49 | ## Selection (Models) 50 | 51 | Models are collections of objects that can be queried in standardized ways and represent standardized *selection*, *query* or *read* operations. 52 | 53 | ### Single Object Selection 54 | 55 | If one or more fields can uniquely identify an object in a model, a root field should allow querying the object using those fields. 56 | 57 | #### Example 58 | 59 | ```graphql 60 | type Query { 61 | user_by_id(user_id: Int!): User 62 | } 63 | ``` 64 | 65 | ### Multi Object Selection 66 | 67 | To fetch multiple objects, a root field should return a list of objects. 68 | 69 | #### Example 70 | 71 | ```graphql 72 | type Query { 73 | users: [User] 74 | } 75 | ``` 76 | 77 | By default semantics, this should return all the objects in the model. Additional input paramenters can control the set of objects returned by the model. 78 | 79 | ## Filtering 80 | 81 | A `where` input parameter for a model allows querying a model based on some condition on one or more of its fields. 82 | This includes any [relationship](#sec-Relationships) fields that have been defined on the object. 83 | 84 | A particular field will support specific comparison operators. 85 | The `where` input paramenter should support operators for AND / OR / NOT to compose a filter expression. 86 | 87 | ### Example 88 | 89 | ```graphql 90 | type Query { 91 | users(where: User_boolean_exp): [User] 92 | } 93 | 94 | input User_boolean_exp { 95 | _and: [User_boolean_exp!] 96 | _or: [User_boolean_exp!] 97 | _not: User_boolean_exp 98 | user_id: Int_comparison_exp 99 | first_name: String_comparison_exp 100 | last_name: String_comparison_exp 101 | email: String_comparison_exp 102 | } 103 | 104 | input Int_comparison_exp { 105 | _eq: Int 106 | _lt: Int 107 | _gt: Int 108 | _lte: Int 109 | _gte: Int 110 | _in: [Int!] 111 | } 112 | 113 | input String_comparison_exp { 114 | _eq: String 115 | _in: [Int!] 116 | _like: String 117 | } 118 | ``` 119 | 120 | ### Filter expression grammar 121 | 122 | Where : ModelBooleanExpression 123 | 124 | ModelBooleanExpression : 125 | - And ModelBooleanExpression+ 126 | - Or ModelBooleanExpression+ 127 | - Not ModelBooleanExpression 128 | - FieldComparisonExpression 129 | 130 | FieldComparisonExpression : Field Operator UserInputValue 131 | 132 | Field : 133 | - ObjectField1 134 | - ObjectField2 135 | - ObjectField3 136 | - ... 137 | 138 | ## Sorting 139 | 140 | OpenDD can generate an input for sorting the objects when querying a model based on one or more of its fields. This includes any relationship fields that have been defined on the object. Multiple fields can be used as the sort key, with the order of specification determining which one gets applied first. 141 | 142 | ### Example 143 | 144 | ```graphql 145 | type Query { 146 | users(where: [User_order_by_exp]): [User] 147 | } 148 | 149 | input User_order_by_exp { 150 | user_id: OrderByDirection 151 | first_name: OrderByDirection 152 | last_name: OrderByDirection 153 | email: OrderByDirection 154 | } 155 | 156 | enum OrderByDirection { 157 | Asc, 158 | Desc 159 | } 160 | ``` 161 | 162 | ## Pagination 163 | 164 | OpenDD can generate input arguments for paginating through the objects returned when querying a model. 165 | 166 | ### Example 167 | 168 | ```graphql 169 | type Query { 170 | users(limit: Int, offset: Int): [User] 171 | } 172 | ``` 173 | 174 | ## Aggregation 175 | 176 | **TODO:** Add after v3 supports aggregation. 177 | 178 | ## Relay 179 | 180 | The GraphQL API generated by OpenDD implements the Relay API and has a `node` query root field that can be used to retrieve specific objects using their global ID. 181 | 182 | ### Example 183 | 184 | ```graphql 185 | type Query { 186 | node(id: ID!): Node 187 | } 188 | ``` 189 | 190 | ## Commands 191 | 192 | Commands are functions which take in some arguments and produce an output. The semantics of commands except for arguments and output are opaque. 193 | 194 | Commands can be made available for invocation either at the query root or the mutation root of the command GraphQL API. 195 | 196 | ### Example 197 | 198 | ```graphql 199 | type Mutation { 200 | validateDiscountCoupon(coupon_code: String): bool 201 | } 202 | ``` 203 | 204 | ## Relationships 205 | 206 | Objects can be augmented with related information by defining a relationship from the object to a model or a command. This way, queries can be flexibly composed. 207 | 208 | Creating a relationship adds a relationship field to the generated object type which can be used to query the related model or command. 209 | 210 | ### Example 211 | 212 | ```graphql 213 | type Order { 214 | order_id: Int! 215 | product_id: Int! 216 | user_id: Int! 217 | user: User // relationship from Order.user_id to User.user_id 218 | } 219 | ``` 220 | 221 | ## Query Composition 222 | 223 | For a has-many (array) relationship, the input parameters of multi-object model selection are available to use, so queries across relationships can be flexibly composed. 224 | 225 | ### Example 226 | 227 | If there is a has-many relationship from User to orders and a has-one (object) relationship from Order to users: 228 | 229 | ```graphql 230 | type User { 231 | user_id: Int! 232 | name: String 233 | orders( 234 | where: Orders_bool_exp, 235 | order_by: Orders_order_by_exp, 236 | limit: Int, 237 | offset: Int 238 | ): [Order] 239 | } 240 | ``` 241 | 242 | Because relationships, filtering, and sorting are semantically meaningful in OpenDD, this allows for efficiently issuing such composable queries without resorting to N+1 database queries, even when the two related models come from different databases. 243 | 244 | ### Example 245 | 246 | The following query results in only 2 database lookups, one for users and one for orders, as opposed to 11 lookups if resolving traditionally. Such efficient composition is impossible to achieve with generic resolver-based approaches even when using data loaders. 247 | 248 | ```graphql 249 | query { 250 | users(limit: 10) { 251 | user_id 252 | orders(where: { total: { _gt: 100 } }, order_by: { order_date: DESC }) { 253 | order_id 254 | product_id 255 | } 256 | } 257 | } 258 | ``` 259 | 260 | ## Filtering / Sorting Composition 261 | 262 | Relationship fields themselves can be used for filtering, sorting, and aggregating a model containing the source object. 263 | 264 | ### Examples 265 | 266 | To query all users who ordered a particular product where orders are not a part of the users model: 267 | 268 | ```graphql 269 | query { 270 | users(where: { orders: { product_id: { _eq: "product_1" } } }) { 271 | user_id 272 | } 273 | } 274 | ``` 275 | 276 | To sort all orders of a particular product by the email of the user that placed the order, where the user email is not a part of the orders model: 277 | 278 | ```graphql 279 | query { 280 | orders(where: { product_id: { _eq: 1 } }, order_by: { user: { email: ASC } }) { 281 | order_id 282 | } 283 | } 284 | ``` 285 | 286 | This is possible since OpenDD has a semantic understanding of relationships and predicates. Doing this would be impossible in generic resolver-based approaches. 287 | --------------------------------------------------------------------------------