├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── .vscode └── tasks.json ├── LICENSE.md ├── README.md ├── example ├── .gitignore ├── README.md ├── migrations │ ├── 0001_database.migration │ ├── 0002_users.migration │ ├── 0003_posts.migration │ └── 0004_graph.migration ├── package-lock.json ├── package.json └── server.js ├── jest.config.js ├── jest.integration.config.js ├── package-lock.json ├── package.json ├── src ├── __tests__ │ ├── config │ │ └── setup.ts │ ├── fixtures │ │ └── typeDefs.ts │ └── queryTranslation.test.ts ├── buildQuery.ts ├── builderInstanceCreators.ts ├── builders │ ├── aql.ts │ ├── aqlDocument.ts │ ├── aqlEdge.ts │ ├── aqlEdgeNode.ts │ ├── aqlId.ts │ ├── aqlKey.ts │ ├── aqlNode.ts │ ├── aqlRelayConnection.ts │ ├── aqlRelayEdges.ts │ ├── aqlRelayNode.ts │ ├── aqlRelayPageInfo.ts │ ├── aqlSubquery.ts │ └── index.ts ├── constants.ts ├── errors.ts ├── executeQuery.ts ├── extractQueries.ts ├── index.ts ├── logger.ts ├── resolver.ts ├── runCustomQuery.ts ├── runQuery.ts ├── typeDefs.ts ├── types.ts └── utils │ ├── __tests__ │ └── plugins.test.ts │ ├── aql.ts │ ├── directives.ts │ ├── graphql.ts │ ├── plugins.ts │ ├── strings.ts │ └── variables.ts ├── tsconfig.json └── types └── index.d.ts /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [8.x, 10.x, 12.x] 18 | 19 | steps: 20 | - uses: actions/checkout@v1 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - name: npm install, build, and test 26 | run: | 27 | npm ci 28 | npm run build --if-present 29 | npm test 30 | env: 31 | CI: true 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .rts2_cache_cjs 5 | .rts2_cache_esm 6 | .rts2_cache_umd 7 | dist 8 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "build", 9 | "problemMatcher": [] 10 | }, 11 | { 12 | "type": "npm", 13 | "script": "start", 14 | "problemMatcher": ["$tsc-watch"], 15 | "isBackground": true 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2020 Grant Forrest (@a-type) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-arangodb 2 | 3 | An experimental library for 'translating' GraphQL operations into ArangoDB AQL queries which are designed to fetch all requested data in as few queries as possible. Flexibility is another objective; I want to empower the developer to define exactly how they want their GraphQL schema without being forced into a particular schema shape due to their database structure. 4 | 5 | ## Maintainers Welcome 6 | 7 | I've moved on from the side project which drove me to create this library and I don't actively use it anymore. If there's a feature or bugfix you need, I encourage you to open a PR. If you'd like to take this project in a new direction, you're welcome to fork it! 8 | 9 | ## Example 10 | 11 | ```graphql 12 | type Query { 13 | user(id: ID!): User @aqlDocument(collection: "users", key: "$args.id") 14 | } 15 | 16 | type User { 17 | friends: [FriendOfEdge!]! 18 | @aqlEdge( 19 | collection: "friendOf" 20 | direction: ANY 21 | sort: { property: "name", sortOn: "$field_node" } 22 | ) 23 | } 24 | 25 | type FriendOfEdge { 26 | strength: Int 27 | user: User! @aqlEdgeNode 28 | } 29 | 30 | type Mutation { 31 | createPost(input: PostCreateInput!): Post! 32 | @aqlSubquery( 33 | query: """ 34 | INSERT { title: $args.input.title, body: $args.input.body } INTO posts 35 | """ 36 | return: "NEW" 37 | ) 38 | } 39 | ``` 40 | 41 | ## Code Examples 42 | 43 | For a simple and small example, see the [example directory](./example). 44 | 45 | For a larger scale app that uses this library, check out my now-defunct startup idea [Toast](https://github.com/a-type/toast/tree/master/services/toast-core). The toast-core microservice (linked) drove the entire development of this library and almost every feature is utilized in that codebase. 46 | 47 | ## Known Limitations 48 | 49 | - This library is not designed to run on the Foxx framework. Because Foxx code runs with direct database memory access, the concern of batching up queries is probably not as important, and designing for Foxx posed too many complexities to getting the library working. 50 | - Experimental features, like Relay support, have known shortcomings. See the documentation for those sections. 51 | - Writing database queries inside a GraphQL schema can be error prone! That's just a fact of life for the way this library is implemented. 52 | 53 | ## Table of Contents 54 | 55 | - [graphql-arangodb](#graphql-arangodb) 56 | - [Setup](#setup) 57 | - [Installing](#installing) 58 | - [Directive type definitions](#directive-type-definitions) 59 | - [Adding a Database instance](#adding-a-database-instance) 60 | - [Resolvers](#resolvers) 61 | - [Customizing the resolver](#customizing-the-resolver) 62 | - [Usage](#usage) 63 | - [Enums](#enums) 64 | - [Inputs](#inputs) 65 | - [Interpolations](#interpolations) 66 | - [Directives](#directives) 67 | - [`@aqlDocument`](#aqldocument) 68 | - [`@aqlNode`](#aqlnode) 69 | - [`@aqlEdge/@aqlEdgeNode`](#aqledgeaqledgenode) 70 | - [`@aqlSubquery`](#aqlsubquery) 71 | - [`@aql`](#aql) 72 | - [`@aqlKey`](#aqlkey) 73 | - [Relay Directives (Experimental)](#relay-directives-experimental) 74 | - [`@aqlRelayConnection`](#aqlrelayconnection) 75 | - [`@aqlRelayEdges`](#aqlrelayedges) 76 | - [`@aqlRelayPageInfo`](#aqlrelaypageinfo) 77 | - [`@aqlRelayNode`](#aqlrelaynode) 78 | - [Running Custom Queries (Experimental)](#running-custom-queries-experimental) 79 | - [Mutations (Experimental)](#mutations-experimental) 80 | - [Development](#development) 81 | - [Local Development](#local-development) 82 | - [`npm start` or `yarn start`](#npm-start-or-yarn-start) 83 | - [`npm run build` or `yarn build`](#npm-run-build-or-yarn-build) 84 | - [`npm test` or `yarn test`](#npm-test-or-yarn-test) 85 | 86 | ## Setup 87 | 88 | ### Installing 89 | 90 | Start by installing the library 91 | 92 | ``` 93 | npm i --save graphql-arangodb 94 | ``` 95 | 96 | You may also need to install peer dependencies if you don't have them: 97 | 98 | ``` 99 | npm i --save graphql arangojs 100 | ``` 101 | 102 | ### Directive type definitions 103 | 104 | To use the directives in this library, you need to add type definitions for them. The library exports pre-built type definitions for all directives, you just need to include them in your type definitions. 105 | 106 | ```ts 107 | import { directiveTypeDefs } from 'graphql-arangodb'; 108 | 109 | const typeDefs = [directiveTypeDefs, ...allYourAppsOtherTypeDefs]; 110 | 111 | makeExecutableSchema({ typeDefs }); 112 | ``` 113 | 114 | ### Adding a Database instance 115 | 116 | The easiest way to connect `graphql-arangodb` to your ArangoDB database is to instantiate a `Database` class from `arangojs` and assign it to the `arangoDb` field of your GraphQL `context`: 117 | 118 | ```ts 119 | const arangoDb = new Database({ 120 | url: 'http://localhost:8529', 121 | }); 122 | arangoDb.useDatabase('mydb'); 123 | arangoDb.useBasicAuth('mysecretuser', 'mysecretpassword'); 124 | 125 | const context = { 126 | arangoDb, 127 | }; 128 | 129 | // pass the context into your GraphQL server according to documentation of the server 130 | ``` 131 | 132 | ### Resolvers 133 | 134 | To start resolving queries using AQL, you need to set up resolvers for fields which will be resolved using those queries. For most use cases, this means all of the top-level fields in the root query and mutation types. 135 | 136 | For most people, adding the default `aqlResolver` from `graphql-arangodb` should be enough: 137 | 138 | ```ts 139 | import aqlResolver from 'graphql-arangodb'; 140 | 141 | const resolvers = { 142 | Query: { 143 | user: aqlResolver, 144 | users: aqlResolver, 145 | // ... 146 | }, 147 | }; 148 | ``` 149 | 150 | #### Customizing the resolver 151 | 152 | However, there are some advanced scenarios where you may want to customize how the resolver works. To do this, you can import `createResolver` and create your own version of the default resolver. All config properties are optional. 153 | 154 | ```ts 155 | import { createResolver, plugins as defaultPlugins } from 'graphql-arangodb'; 156 | 157 | const resolver = createResolver({ 158 | // argument resolvers are called like regular resolvers, but they are used only by 159 | // graphql-arangodb to apply custom transformations to field arguments before 160 | // adding them to the AQL query. They are separated from normal resolvers for 161 | // technical reasons related to how queries are extracted and built by the library. 162 | // Whenver possible, prefer to put this logic inside the AQL query itself. 163 | argumentResolvers: { 164 | Query: { 165 | searchUsers: args => ({ 166 | ...args, 167 | // apply Lucene fuzzy indicator to user's match string before passing it to AQL 168 | match: `${args.match}~`, 169 | }), 170 | }, 171 | }, 172 | 173 | // customize the key in your context which stores data which will be passed down 174 | // into AQL queries via the $context interpolation 175 | contextKey: 'arango_context', 176 | 177 | // customize the context property which is used to get your Database instance 178 | contextDbKey: 'arango_db', 179 | 180 | // advanced: you can reassign the names of the default directive plugins, or 181 | // create your own plugin here. Plugins aren't documented yet, see source. 182 | plugins: { 183 | ...defaultPlugin, 184 | custom: myCustomPlugin, 185 | }, 186 | 187 | // you can specify a static database instance instead of passing one through context 188 | db: new Database(), 189 | }); 190 | ``` 191 | 192 | ## Usage 193 | 194 | Now that the library is configured, you can start adding directives to indicate how to query for your data. 195 | 196 | Usage of these directives is fairly similar to writing subqueries directly in AQL. The main thing to know is that you never write the `RETURN` statement. This library automatically constructs the correct `RETURN` projections based on the selected fields in the GraphQL query. 197 | 198 | ### Enums 199 | 200 | Before we begin with the directives, this library also ships some enums which will be used in directive parameters. To use an enum, just supply its literal value to the parameter (don't enclose it in `"` marks). 201 | 202 | - `AqlEdgeDirection`: `OUTBOUND | INBOUND | ANY` 203 | - `AqlSortOrder`: `DESC | ASC` 204 | - `AqlRelayConnectionSource`: `Default | FullText` 205 | 206 | ### Inputs 207 | 208 | Some directives take complex inputs: 209 | 210 | ```graphql 211 | input AqlSortInput { 212 | """ 213 | The property to sort on 214 | """ 215 | property: String! 216 | """ 217 | The order to sort in. Defaults ASC 218 | """ 219 | order: AqlSortOrder = ASC 220 | """ 221 | Change the object being sorted. Defaults to $field 222 | """ 223 | sortOn: String 224 | } 225 | 226 | input AqlLimitInput { 227 | """ 228 | The upper limit of documents to return 229 | """ 230 | count: String! 231 | """ 232 | The number of documents to skip 233 | """ 234 | skip: String 235 | } 236 | 237 | """ 238 | These are the same as the OPTIONS for a regular edge traversal in AQL 239 | """ 240 | input AqlTraversalOptionsInput { 241 | bfs: Boolean 242 | uniqueVertices: String 243 | uniqueEdges: String 244 | } 245 | ``` 246 | 247 | ### Interpolations 248 | 249 | All directives support the following interpolations in their parameter values: 250 | 251 | - `$parent`: Reference the parent document. If there is no parent (this is a root field in the query), references the `parent` from GraphQL, if that exists. 252 | - `$field`: Reference the field itself. In `@aql` directives, you must assign something to this binding to be returned as the value of the field. For all other purposes, you can use this to reference the current value (for instance, if you want to do a filter on `$field.name` or some other property). 253 | - `$args`: Reference the field args of the GraphQL query. You can use nested arg values. Usages of `$args` get turned into bind variables when the query is executed, and all field args are passed in as values. 254 | - `$context`: Reference values from the `arangoContext` key in your GraphQL context. Use this for global values across all queries, like the authenticated user ID. 255 | 256 | ### Directives 257 | 258 | #### `@aqlDocument` 259 | 260 | Selects a single or multiple documents (depending on whether the return type of the field is a list) from a specified collection. If a single document is selected, you can supply an `key` parameter to select it directly. This `key` parameter may be an argument interpolation (`$args.id`, etc), or a concrete value. It is passed directly into the `DOCUMENT` AQL function as the second parameter. If you do not specify an `key` parameter, the first item from the collection will be returned. To select a single item with a filter, use `@aql`. 261 | 262 | **Parameters** 263 | 264 | - `collection: String!`: The name of the collection of documents 265 | - `key: String`: A string value or interpolation that indicates the database key of the document. 266 | - `filter: String`: Adds a filter expression. Applies to key-based single document fetching (the first document will be taken after filter is applied). 267 | - `sort: AqlSortInput`: Adds a sort expression. Applies to key-based single document fetching (the first document will be taken after sort is applied). 268 | - `limit: AqlLimitInput`: Adds a limit expression. Only works when `key` is not provided. 269 | 270 | **Example** 271 | 272 | ```graphql 273 | type Query { 274 | user(id: ID!): User @aqlDocument(collection: "users", key: "$args.id") 275 | } 276 | ``` 277 | 278 | #### `@aqlNode` 279 | 280 | Traverses a relationship from the parent document to another document across an edge. `@aqlNode` skips over the edge and returns the related document as the field value. If you want to utilize properties from the edge, use `@aqlEdge/@aqlEdgeNode` instead. 281 | 282 | **Parameters** 283 | 284 | - `edgeCollection: String!`: The name of the collection which the edge belongs to 285 | - `direction: AqlEdgeDirection!`: The direction to traverse. Can be `ANY`. 286 | - `filter: String`: Adds a filter expression. 287 | - `sort: AqlSortInput`: Adds a sort expression. 288 | - `limit: AqlLimitInput`: Adds a limit expression. 289 | - `options: AqlTraverseOptionsInput`: Modify OPTIONS parameters on the traversal. 290 | 291 | **Example** 292 | 293 | ```graphql 294 | type User { 295 | posts: [Post!]! @aqlNode(edgeCollection: "posted", direction: OUTBOUND) 296 | } 297 | ``` 298 | 299 | #### `@aqlEdge/@aqlEdgeNode` 300 | 301 | `@aqlEdge` traverses an edge from the parent document, returning the edge itself as the field value. `@aqlEdgeNode` can be used on the type which represents the edge to reference the document at the other end of it. `@aqlEdgeNode` should only be used on a field within a type represented by an edge. It has no directive parameters. 302 | 303 | **Parameters** 304 | 305 | Only `@aqlEdge` takes parameters: 306 | 307 | - `collection: String!`: The name of the collection for the edge 308 | - `direction: AqlEdgeDirection!`: The direction to traverse. Can be `ANY`. 309 | - `filter: String`: Adds a filter expression. To filter on the node, you can use `$field_node` as an interpolation. Defaults `sortOn` to `$field`. 310 | - `sort: AqlSortInput` Adds a sort expression. 311 | - `limit: AqlLimitInput`: Adds a limit expression. 312 | - `options: AqlTraverseOptionsInput`: Modify OPTIONS parameters on the traversal. 313 | 314 | `@aqlEdgeNode` has no parameters. 315 | 316 | **Example** 317 | 318 | ```graphql 319 | type User { 320 | friends: [FriendOfEdge!]! 321 | @aqlEdge( 322 | collection: "friendOf" 323 | direction: ANY 324 | sort: { property: "name", sortOn: "$field_node" } 325 | ) 326 | } 327 | 328 | type FriendOfEdge { 329 | strength: Int 330 | user: User! @aqlEdgeNode 331 | } 332 | ``` 333 | 334 | #### `@aqlSubquery` 335 | 336 | Construct a free-form subquery to resolve a field. There are important rules for your subquery: 337 | 338 | - **Important**: You must assign the value you wish to resolve to the `$field` binding. This can be done for a single value using `LET $field = value`, or for a list by ending the subquery with `FOR $field IN list`. See the examples. 339 | - Do not wrap in `()`. This is done by the library. 340 | - Do not include a `RETURN` statement. All `RETURN` projections are constructed by the library for you to match the GraphQL query. 341 | 342 | **Parameters** 343 | 344 | - `query: String!`: Your subquery string, following the rules listed above. 345 | - `return: String`: An optional way to specify the name of a binding to return. By default, in a subquery, you must follow the important rule marked above and assign to `$field`. However, if you prefer, you may specify which variable binding you want to return within your subquery, and we will do this for you. 346 | 347 | **Examples** 348 | 349 | _Resolving a single value_ 350 | 351 | ```graphql 352 | type Query { 353 | userCount: Int! 354 | @aqlSubquery( 355 | query: """ 356 | LET $field = LENGTH(users) 357 | """ 358 | ) 359 | } 360 | ``` 361 | 362 | _Resolving multiple values_ 363 | 364 | ```graphql 365 | type Query { 366 | """ 367 | Merges the list of public posts with the list of posts the user has posted (even 368 | private) to create a master list of all posts accessible by the user. 369 | """ 370 | authorizedPosts: [Post!]! 371 | @aqlSubquery( 372 | query: """ 373 | LET authenticatedUser = DOCUMENT('users', $context.userId) 374 | LET allAuthorizedPosts = UNION_DISTINCT( 375 | (FOR post IN posts FILTER post.public == true RETURN post), 376 | (FOR post in OUTBOUND authenticatedUser posted RETURN post) 377 | ) 378 | FOR $field in allAuthorizedPosts 379 | """ 380 | ) 381 | } 382 | ``` 383 | 384 | In the above example, instead of the final line, you could also pass `"allAuthorizedPosts"` to the `return` parameter: 385 | 386 | ```graphql 387 | type Query { 388 | """ 389 | Merges the list of public posts with the list of posts the user has posted (even 390 | private) to create a master list of all posts accessible by the user. 391 | """ 392 | authorizedPosts: [Post!]! 393 | @aqlSubquery( 394 | query: """ 395 | LET authenticatedUser = DOCUMENT('users', $context.userId) 396 | LET allAuthorizedPosts = UNION_DISTINCT( 397 | (FOR post IN posts FILTER post.public == true RETURN post), 398 | (FOR post in OUTBOUND authenticatedUser posted RETURN post) 399 | ) 400 | """ 401 | return: "allAuthorizedPosts" 402 | ) 403 | } 404 | ``` 405 | 406 | #### `@aql` 407 | 408 | Free-form AQL for resolving individual fields using parent data or arbitrary expressions. Unlike `@aqlSubquery`, this should not be used for a full query structure, only for a simple expression. 409 | 410 | **Parameters** 411 | 412 | - `expression: String!`: The expression to evaluate. Use interpolations to access in-scope information, like the `$parent`. 413 | 414 | **Example** 415 | 416 | ```graphql 417 | type User { 418 | fullName: String! 419 | @aql(expression: "CONCAT($parent.firstName, \" \", $parent.lastName)") 420 | } 421 | ``` 422 | 423 | #### `@aqlKey/@aqlId` 424 | 425 | Resolve the annotated field with the `_key` or `_id` of the parent document, respectively. You can just attach these to any field which indicates the type's `ID` if you want your GraphQL IDs to be based on the underlying ArangoDB keys or full IDs. 426 | 427 | **Example** 428 | 429 | ```graphql 430 | type User { 431 | id: ID @aqlKey # will be "2301" or similar 432 | } 433 | 434 | type Post { 435 | id: ID @aqlId # will be "posts/1234" or similar (depending on your collection name) 436 | } 437 | ``` 438 | 439 | ### Relay Directives (Experimental) 440 | 441 | **Known limitations** 442 | 443 | The current Relay directives don't conform entirely to the Relay spec. They only support `first`/`after` paging; no reverse paging. `pageInfo` does not include `hasPreviousPage`. They work for basic, forward-looking pagination use cases, but have not been tested with the official Relay client library. 444 | 445 | > The usage of these directives may change a bit over time, so be sure to check when upgrading the library! 446 | 447 | You must use all of the provided directives to properly construct a Relay connection, according to the rules below. The following example provides a full picture of how to create a Relay Connection: 448 | 449 | **Basic Relay Example** 450 | 451 | ```graphql 452 | type User { 453 | postsConnection(first: Int = 10, after: String): UserPostsConnection! 454 | @aqlRelayConnection( 455 | edgeCollection: "posted" 456 | edgeDirection: OUTBOUND 457 | cursorExpression: "$node.title" 458 | ) 459 | } 460 | 461 | type UserPostsConnection { 462 | edges: [UserPostEdge!]! @aqlRelayEdges 463 | pageInfo: UserPostsPageInfo! @aqlRelayPageInfo 464 | } 465 | 466 | type UserPostEdge { 467 | cursor: String! 468 | node: Post! @aqlRelayNode 469 | } 470 | 471 | type UserPostsPageInfo { 472 | hasNextPage: Boolean! 473 | } 474 | 475 | type Post { 476 | id: ID! 477 | title: String! 478 | body: String! 479 | publishedAt: String! 480 | } 481 | ``` 482 | 483 | **Relay Example with filtering** 484 | 485 | ```graphql 486 | type User { 487 | postsConnection( 488 | first: Int = 10 489 | after: String 490 | filter: PostsFilterInput 491 | ): UserPostsConnection! 492 | @aqlRelayConnection( 493 | edgeCollection: "posted" 494 | edgeDirection: OUTBOUND 495 | cursorExpression: "$node.title" 496 | filter: """ 497 | ($args['filter'] && ( 498 | $args['filter'].titleLike == null || LIKE($node.title, CONCAT("%", $args['filter'].titleLike, "%")) 499 | ) && ( 500 | $args['filter'].publishedAfter == null || $node.publishedAt > $args['filter'].publishedAfter 501 | )) 502 | """ 503 | ) 504 | } 505 | 506 | input PostsFilterInput { 507 | titleLike: String 508 | publishedAfter: String 509 | } 510 | ``` 511 | 512 | _About filtering_ 513 | 514 | - The `filter` parameter must be evaluated as a single boolean expression. Outer parameters should be used to enclose multiple computations. 515 | - If your filter parameter is optional, you should guard against it being `null` within your filter statement. 516 | - The word `filter` is interpreted in AQL as a new `FILTER` statement, so if you use that as a parameter name, you must access it via bracket syntax (`['filter']`), not dot syntax (`.filter`) 517 | - Test that the user has supplied a filterable value before filtering on that value (this is the reason the above example tests that `$args['filter'].titleLike` is not null before asserting that the node title is LIKE that value) 518 | - You may use `$node` and `$edge` to represent the current node and edge you are filtering against. `$edge` is only valid in a true edge connection from a parent node. 519 | 520 | All directives can be applied to either the field which is resolved, or the type it resolves to. Applying the directive to the type might be useful if you reuse the connection in multiple places and don't want to apply the directive to each one. However, doing so may make your schema harder to read. 521 | 522 | #### `@aqlRelayConnection` 523 | 524 | Add this directive to a field _or_ type definition to indicate that it should be resolved as a Relay Connection. The resolved value will have the standard `edges` and `pageInfo` parameters. 525 | 526 | > Note: Currently this only supports forward pagination using `after`. 527 | 528 | **Parameters** 529 | 530 | - `edgeCollection: String`: The name of the collection of edges to traverse 531 | - `edgeDirection: AqlEdgeDirection`: The direction to traverse edges. Can be `ANY`. 532 | - `cursorExpression: String`: An expression used to compute a cursor from a node or edge. Using `$node` will refer to the node, `$edge` refers to the edge. If omitted, entries will be sorted by `_key`. 533 | - `filter: String`: Supply a filter statement to further reduce the edges which will be matched in the connection. `$node`, `$edge`, and `$path` may be used in addition to all standard interpolations, and will correspond to the first, second and third positional bindings in a `FOR ... IN` edge traversal statement. 534 | - `source: String`: (Advanced) Supply your own custom `FOR` expression to source documents from. For example, `FOR $node IN FULLTEXT(Posts, "title", $args.searchTerm)` would create a fulltext search connection. Use `$node` and `$edge` as bindings when traversing documents so that the rest of the query works properly. It's also possible to use subqueries to traverse more advanced collections, like `FOR $node IN (FOR foo IN ...)`. Using a subquery in this way is valid AQL, so you can place any complex traversal logic within it if you wish. Also, if you use `$edge` or `$path` in your `filter` or `cursorExpression` arg, you should be sure to bind them in your `source` arg! 535 | 536 | #### `@aqlRelayEdges` 537 | 538 | Add this directive to a field _or_ type definition to indicate that it should be resolved as a Relay Edge list. Must be used as a child field of a type resolved by `@aqlRelayConnection`. 539 | 540 | #### `@aqlRelayPageInfo` 541 | 542 | Add this directive to a field _or_ type definition to indicate that it should be resolved as a Relay Page Info object. Must be used as a child field of a type resolved by `@aqlRelayConnection`. 543 | 544 | #### `@aqlRelayNode` 545 | 546 | Add this directive to a field _or_ type definition to indicate that it should be resolved as the Node of a Relay Edge. Must be used as a child field of a type resolved by `@aqlRelayEdge`. 547 | 548 | ### Running Custom Queries (Experimental) 549 | 550 | In addition to adding directives to your schema to resolve fields, you can also utilize a function called `runCustomQuery` to imperatively execute AQL queries like you would using the standard `arangojs` client, but with added support for projected return values based on the GraphQL selection! 551 | 552 | If that doesn't make sense, imagine a scenario where you are writing a query to do a full text search and you want to pre-process the user's input to work with Lucene. There's not currently a great place to put that processing logic; all the `@aql` directives assume you're just passing in the user's arguments verbatim. 553 | 554 | Instead, you can write your own resolver like so: 555 | 556 | ```ts 557 | import aqlResolver from 'graphql-arangodb'; 558 | import aql from 'arangojs'; 559 | 560 | const searchResolver = async (parent, args, context, info) => { 561 | const fullTextSearchString = processSearchString(args.searchString); 562 | 563 | return aqlResolver.runCustomQuery({ 564 | query: aql` 565 | FOR matchedPost IN FULLTEXT(posts, "title", ${fullTextSearchString}) 566 | RETURN matchedPost 567 | `, 568 | parent, 569 | args, 570 | context, 571 | info, 572 | }); 573 | }; 574 | ``` 575 | 576 | Here we're using the `aqlResolver.runCustomQuery` function, which accepts a custom query string and bind variables. Write your own AQL however you'd like and return the data to resolve the current field (but be aware that your AQL will be run inside a larger query!). 577 | 578 | The magic comes in when the result is returned. Because you passed in the `parent`, `context`, and `info`, `graphql-arangodb` can extend your query to return the rest of the data the user needs for their GraphQL operation. In other words, if the user made the query: 579 | 580 | ```graphql 581 | query Search($searchString: "good") { 582 | search(searchString: $searchString) { 583 | id 584 | title 585 | body 586 | 587 | tags { 588 | id 589 | name 590 | } 591 | 592 | author { 593 | id 594 | name 595 | } 596 | } 597 | } 598 | ``` 599 | 600 | ... they would still get `tags` and `author` resolved by your existing `@aql` directives on your schema, at no cost to you. 601 | 602 | `runCustomQuery` is a tool to give you as much power as possible to craft root queries and mutations, while still getting the benefits of your declarative directives to resolve deeply nested data in a single database round-trip. 603 | 604 | #### Using the built-in query builders 605 | 606 | In addition to crafting your own queries with a literal string, you can still use this library's built-in 'query builders' which power the directives to create your custom query. This enables you to either opt out of using directives entirely (if you prefer not to clutter your schema document) or conditionally trigger different built-in behaviors. 607 | 608 | ```ts 609 | import aqlResolver, { builders } from 'graphql-arangodb'; 610 | 611 | const conditionalResolver = async (parent, args, context, info) => { 612 | if (args.searchTerm) { 613 | return aqlResolver.runCustomQuery({ 614 | queryBuilder: builders.aqlRelayConnection({ 615 | // this sets up the relay connection to draw from a search view using the requested search term 616 | source: `FOR $node IN PostSearchView SEARCH PHRASE($node.name, $args.searchTerm, 'text_en')`, 617 | // our 'cursor' will actually be the weight value of the result, allowing proper sorting of results by weight. 618 | cursorExpression: `BM25($node)`, 619 | // because we order by weight, we actually want to start at higher values and go down 620 | sortOrder: 'DESC', 621 | }), 622 | parent, 623 | args, 624 | context, 625 | info, 626 | }); 627 | } else { 628 | return aqlResolver.runCustomQuery({ 629 | queryBuilder: builders.aqlRelayConnection({ 630 | source: `FOR $node IN posts`, 631 | cursorExpression: '$node.createdAt', 632 | }), 633 | parent, 634 | args, 635 | context, 636 | info, 637 | }); 638 | } 639 | }; 640 | ``` 641 | 642 | With the custom resolver above, for example, we construct our Relay-style connection based on a search view if the user has supplied a search term argument, or else we simply list all documents in the collection. 643 | 644 | ### Mutations (Experimental) 645 | 646 | Simple mutations are essentially made possible using the same tools as queries, especially `@aqlSubquery`: 647 | 648 | ```graphql 649 | type Mutation { 650 | createPost(input: PostCreateInput!): Post! 651 | @aqlSubquery( 652 | query: """ 653 | INSERT { title: $args.input.title, body: $args.input.body } INTO posts 654 | """ 655 | return: "NEW" 656 | ) 657 | } 658 | ``` 659 | 660 | The user can, of course, make selections on the returned `Post`, which will be properly converted into projections and subqueries just like a query operation. 661 | 662 | However, there are some limitations to how complex things can get before you want a proper resolver. If there is logic to be done before writing to the database, you can defer calling `graphql-arangodb`'s resolver until you have done it: 663 | 664 | ```ts 665 | import { resolver } from 'graphql-arangodb'; 666 | 667 | const resolvers = { 668 | Mutation: { 669 | createPost: async (parent, args, ctx, info) => { 670 | const canCreatePost = await doSomethingElse(args, ctx); 671 | 672 | if (!canCreatePost) { 673 | throw new ForbiddenError("Hey, you can't do that!"); 674 | } 675 | 676 | return resolver(parent, args, ctx, info); 677 | }, 678 | }, 679 | }; 680 | ``` 681 | 682 | You could also use the same trick to do some logic after. 683 | 684 | If you want to modify the arguments before passing them on, or do even more advanced logic, see [the section on `runCustomQuery`](#running-custom-queries-experimental) above. 685 | 686 | ### Splitting Up Queries (Experimental) 687 | 688 | There are notable use cases where you may want to specifically split the overall GraphQL operation into multiple AQL queries. For instance, if you do a write mutation, ArangoDB will not allow you to read from that collection again in the same query. However, it's possible (depending on what you return from your mutation) for the user to create a selection set which re-traverses collections which were affected by the original write. In such a case, you may want to split the initial write AQL query from the subsequent read queries in the remainder of the operation. 689 | 690 | You can use the experimental `@aqlNewQuery` directive to do this. Simply add it to any field, and that field will start a brand new AQL query, as if it had been a root field. 691 | 692 | **Important:** you must attach the library resolver to any field you annotate with `@aqlNewQuery`, so that it can process that field and any sub-selections into the new AQL query. 693 | 694 | **Important:** if you are using this directive to accomplish a read-after-write scenario, you should add the `waitForSync` option to your write queries to ensure the data is consistent before the second query is run. 695 | 696 | **Example:** 697 | 698 | ```graphql 699 | type Post { 700 | id: ID! @aqlKey 701 | title: String! 702 | body: String! 703 | publishedAt: String! 704 | author: User! @aqlNode(edgeCollection: "posted", direction: INBOUND) 705 | } 706 | 707 | type CreatePostPayload { 708 | post: Post! 709 | @aqlNewQuery 710 | @aqlSubquery( 711 | query: """ 712 | LET $field = DOCUMENT(posts, $parent._key) 713 | """ 714 | ) 715 | } 716 | 717 | type Mutation { 718 | createPost: CreatePostPayload! 719 | @aqlSubquery( 720 | query: """ 721 | INSERT { title: "Fake post", body: "foo", publishedAt: "2019-05-03" } 722 | INTO posts 723 | OPTIONS { waitForSync: true } 724 | LET $field = { 725 | post: NEW 726 | } 727 | """ 728 | ) 729 | } 730 | ``` 731 | 732 | The example above allows a user to make a query like this: 733 | 734 | ```graphql 735 | mutation CreatePost { 736 | createPost { 737 | post { 738 | id 739 | title 740 | author { 741 | id 742 | } 743 | } 744 | } 745 | } 746 | ``` 747 | 748 | without triggering an "access after data-modification by traversal" error from AQL. 749 | 750 | Splitting up queries may also be useful for tuning performance and balancing the overall size of queries. 751 | 752 | #### Splitting queries on relationships 753 | 754 | One interesting property of AQL is that it will interpret a binding parameter which is shaped like a document as a document. This enables you to seamlessly split up fields which traverse edges using `@aqlNewQuery` without any further modifications, because the node from the previous query will be passed into the new query as a `@parent` bind parameter, and all built-in traversal queries are designed to utilize this. In other words, you can add `@aqlNewQuery` to `@aqlNode`, `@aqlEdge`, and `@aqlRelayConnection` without any further changes, and they will function correctly (while splitting into new queries themselves). 755 | 756 | In detail: while a typical `@aqlNode` query, for instance, might look like this when generated (much of this is scaffolding from the library, but pay attention to the simplePosts field subquery): 757 | 758 | ``` 759 | LET query = FIRST( 760 | LET createUser = FIRST( 761 | INSERT {_key: @userId, role: @role, name: @name} INTO users 762 | RETURN NEW 763 | ) 764 | RETURN { 765 | _id: createUser._id, 766 | _key: createUser._key, 767 | _rev: createUser._rev, 768 | name: createUser.name, 769 | id: createUser._key, 770 | simplePosts: ( 771 | FOR createUser_simplePosts IN OUTBOUND createUser posted 772 | RETURN { 773 | _id: createUser_simplePosts._id, 774 | _key: createUser_simplePosts._key, 775 | _rev: createUser_simplePosts._rev, 776 | title: createUser_simplePosts.title, 777 | id: createUser_simplePosts._key 778 | } 779 | ) 780 | } 781 | ) 782 | RETURN query 783 | ``` 784 | 785 | ... if you were to add `@aqlNewQuery` to the `simplePosts` field, it would generate two queries: 786 | 787 | ``` 788 | LET query = FIRST( 789 | LET createUser = FIRST( 790 | INSERT {_key: @userId, role: @role, name: @name} INTO users 791 | RETURN NEW 792 | ) 793 | RETURN { 794 | _id: createUser._id, 795 | _key: createUser._key, 796 | _rev: createUser._rev, 797 | name: createUser.name, 798 | id: createUser._key, 799 | } 800 | ) 801 | RETURN query 802 | ``` 803 | 804 | for the rest of the fields, and then: 805 | 806 | ``` 807 | LET query = FIRST( 808 | FOR createUser_simplePosts IN OUTBOUND @parent posted 809 | RETURN { 810 | _id: createUser_simplePosts._id, 811 | _key: createUser_simplePosts._key, 812 | _rev: createUser_simplePosts._rev, 813 | title: createUser_simplePosts.title, 814 | id: createUser_simplePosts._key 815 | } 816 | ) 817 | RETURN query 818 | ``` 819 | 820 | for the `simplePosts` field. 821 | 822 | The `@parent` bind parameter of the second query will be populated with the returned value from the first query, which includes the needed `_id` field (the library ensures this is always present) for AQL to evaluate the `@parent` bind variable as a document reference. 823 | 824 | If you want to expriment with this behavior on your own, try running an AQL query in your database and passing an object with a valid `_id` field as a bind parameter, then traversing edges from it. 825 | 826 | --- 827 | 828 | ## Development 829 | 830 | This project was bootstrapped with [TSDX](https://github.com/jaredpalmer/tsdx). 831 | 832 | ### Local Development 833 | 834 | Below is a list of commands you will probably find useful. 835 | 836 | #### `npm start` or `yarn start` 837 | 838 | Runs the project in development/watch mode. Your project will be rebuilt upon changes. TSDX has a special logger for you convenience. Error messages are pretty printed and formatted for compatibility VS Code's Problems tab. 839 | 840 | 841 | 842 | Your library will be rebuilt if you make edits. 843 | 844 | #### `npm run build` or `yarn build` 845 | 846 | Bundles the package to the `dist` folder. 847 | The package is optimized and bundled with Rollup into multiple formats (CommonJS, UMD, and ES Module). 848 | 849 | 850 | 851 | #### `npm test` or `yarn test` 852 | 853 | Runs the test watcher (Jest) in an interactive mode. 854 | By default, runs tests related to files changed since the last commit. 855 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | config.yaml 2 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # graphql-arangodb example app 2 | 3 | This example assumes you have an ArangoDB instance running on `localhost:8529` that is accessible with the credentials `username:password`. 4 | 5 | It also assumes you have a database named `exampleDb`. Set that up if you don't. 6 | 7 | Finally, you need these collections: 8 | 9 | - `users` 10 | - `posts` 11 | 12 | You also need a named graph with edge collections: 13 | 14 | - `posted` : from `users`, to `posts`. 15 | - `friendOf`: from `users`, to `users`. 16 | 17 | I like [migo](https://github.com/deusdat/arangomigo) as an ArangoDB migration tool. There's migrations for Migo in the `./migrations` directory. You've got to provide the [config](https://github.com/deusdat/arangomigo#creating-the-configuration-file) for your local database server though. 18 | 19 | ## Playing with the example 20 | 21 | Here are some things to try: 22 | 23 | ```graphql 24 | mutation { 25 | createExampleUser { 26 | user { 27 | name 28 | } 29 | } 30 | } 31 | ``` 32 | 33 | ```graphql 34 | mutation { 35 | createPost(title: "Hello world", body: "Hiiii") { 36 | post { 37 | id 38 | } 39 | } 40 | } 41 | ``` 42 | 43 | ```graphql 44 | query { 45 | users { 46 | id 47 | name 48 | posts { 49 | id 50 | } 51 | drafts { 52 | id 53 | title 54 | } 55 | } 56 | } 57 | ``` 58 | 59 | The new post will show up in drafts, since it doesn't have a publishedAt field. 60 | -------------------------------------------------------------------------------- /example/migrations/0001_database.migration: -------------------------------------------------------------------------------- 1 | type: database 2 | action: create 3 | name: exampleDb 4 | allowed: 5 | - username: username 6 | password: password 7 | -------------------------------------------------------------------------------- /example/migrations/0002_users.migration: -------------------------------------------------------------------------------- 1 | type: collection 2 | action: create 3 | name: users 4 | -------------------------------------------------------------------------------- /example/migrations/0003_posts.migration: -------------------------------------------------------------------------------- 1 | type: collection 2 | action: create 3 | name: posts 4 | -------------------------------------------------------------------------------- /example/migrations/0004_graph.migration: -------------------------------------------------------------------------------- 1 | type: graph 2 | action: create 3 | name: main_graph 4 | edgedefinitions: 5 | - collection: posted 6 | from: 7 | - users 8 | to: 9 | - posts 10 | - collection: friendOf 11 | from: 12 | - users 13 | to: 14 | - users 15 | -------------------------------------------------------------------------------- /example/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/aws-lambda": { 8 | "version": "8.10.13", 9 | "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.13.tgz", 10 | "integrity": "sha512-a1sC60Bqll4N2RYnd4+XuynrVd8LO+uZrgwCVaAER0ldMQ00LRM4iTjU2ulPoQF6P5bHZK5hL/6IF9088VJhUA==" 11 | }, 12 | "@types/body-parser": { 13 | "version": "1.19.0", 14 | "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", 15 | "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", 16 | "requires": { 17 | "@types/connect": "*", 18 | "@types/node": "*" 19 | } 20 | }, 21 | "@types/connect": { 22 | "version": "3.4.33", 23 | "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", 24 | "integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==", 25 | "requires": { 26 | "@types/node": "*" 27 | } 28 | }, 29 | "@types/cors": { 30 | "version": "2.8.6", 31 | "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.6.tgz", 32 | "integrity": "sha512-invOmosX0DqbpA+cE2yoHGUlF/blyf7nB0OGYBBiH27crcVm5NmFaZkLP4Ta1hGaesckCi5lVLlydNJCxkTOSg==", 33 | "requires": { 34 | "@types/express": "*" 35 | } 36 | }, 37 | "@types/express": { 38 | "version": "4.17.6", 39 | "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.6.tgz", 40 | "integrity": "sha512-n/mr9tZI83kd4azlPG5y997C/M4DNABK9yErhFM6hKdym4kkmd9j0vtsJyjFIwfRBxtrxZtAfGZCNRIBMFLK5w==", 41 | "requires": { 42 | "@types/body-parser": "*", 43 | "@types/express-serve-static-core": "*", 44 | "@types/qs": "*", 45 | "@types/serve-static": "*" 46 | } 47 | }, 48 | "@types/express-serve-static-core": { 49 | "version": "4.17.7", 50 | "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.7.tgz", 51 | "integrity": "sha512-EMgTj/DF9qpgLXyc+Btimg+XoH7A2liE8uKul8qSmMTHCeNYzydDKFdsJskDvw42UsesCnhO63dO0Grbj8J4Dw==", 52 | "requires": { 53 | "@types/node": "*", 54 | "@types/qs": "*", 55 | "@types/range-parser": "*" 56 | } 57 | }, 58 | "@types/graphql": { 59 | "version": "14.5.0", 60 | "resolved": "https://registry.npmjs.org/@types/graphql/-/graphql-14.5.0.tgz", 61 | "integrity": "sha512-MOkzsEp1Jk5bXuAsHsUi6BVv0zCO+7/2PTiZMXWDSsMXvNU6w/PLMQT2vHn8hy2i0JqojPz1Sz6rsFjHtsU0lA==", 62 | "requires": { 63 | "graphql": "*" 64 | } 65 | }, 66 | "@types/graphql-deduplicator": { 67 | "version": "2.0.0", 68 | "resolved": "https://registry.npmjs.org/@types/graphql-deduplicator/-/graphql-deduplicator-2.0.0.tgz", 69 | "integrity": "sha512-swUwj5hWF1yFzbUXStLJrUa0ksAt11B8+SwhsAjQAX0LYJ1LLioAyuDcJ9bovWbsNzIXJYXLvljSPQw8nR728w==" 70 | }, 71 | "@types/mime": { 72 | "version": "2.0.2", 73 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.2.tgz", 74 | "integrity": "sha512-4kPlzbljFcsttWEq6aBW0OZe6BDajAmyvr2xknBG92tejQnvdGtT9+kXSZ580DqpxY9qG2xeQVF9Dq0ymUTo5Q==" 75 | }, 76 | "@types/node": { 77 | "version": "14.0.13", 78 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.13.tgz", 79 | "integrity": "sha512-rouEWBImiRaSJsVA+ITTFM6ZxibuAlTuNOCyxVbwreu6k6+ujs7DfnU9o+PShFhET78pMBl3eH+AGSI5eOTkPA==" 80 | }, 81 | "@types/qs": { 82 | "version": "6.9.3", 83 | "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.3.tgz", 84 | "integrity": "sha512-7s9EQWupR1fTc2pSMtXRQ9w9gLOcrJn+h7HOXw4evxyvVqMi4f+q7d2tnFe3ng3SNHjtK+0EzGMGFUQX4/AQRA==" 85 | }, 86 | "@types/range-parser": { 87 | "version": "1.2.3", 88 | "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", 89 | "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" 90 | }, 91 | "@types/serve-static": { 92 | "version": "1.13.4", 93 | "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.4.tgz", 94 | "integrity": "sha512-jTDt0o/YbpNwZbQmE/+2e+lfjJEJJR0I3OFaKQKPWkASkCoW3i6fsUnqudSMcNAfbtmADGu8f4MV4q+GqULmug==", 95 | "requires": { 96 | "@types/express-serve-static-core": "*", 97 | "@types/mime": "*" 98 | } 99 | }, 100 | "@types/zen-observable": { 101 | "version": "0.5.4", 102 | "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.5.4.tgz", 103 | "integrity": "sha512-sW6xN96wUak4tgc89d0tbTg7QDGYhGv5hvQIS6h4mRCd8h2btiZ80loPU8cyLwsBbA4ZeQt0FjvUhJ4rNhdsGg==" 104 | }, 105 | "@wry/equality": { 106 | "version": "0.1.11", 107 | "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.1.11.tgz", 108 | "integrity": "sha512-mwEVBDUVODlsQQ5dfuLUS5/Tf7jqUKyhKYHmVi4fPB6bDMOfWvUPJmKgS1Z7Za/sOI3vzWt4+O7yCiL/70MogA==", 109 | "requires": { 110 | "tslib": "^1.9.3" 111 | } 112 | }, 113 | "accepts": { 114 | "version": "1.3.7", 115 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 116 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", 117 | "requires": { 118 | "mime-types": "~2.1.24", 119 | "negotiator": "0.6.2" 120 | } 121 | }, 122 | "apollo-cache-control": { 123 | "version": "0.1.1", 124 | "resolved": "https://registry.npmjs.org/apollo-cache-control/-/apollo-cache-control-0.1.1.tgz", 125 | "integrity": "sha512-XJQs167e9u+e5ybSi51nGYr70NPBbswdvTEHtbtXbwkZ+n9t0SLPvUcoqceayOSwjK1XYOdU/EKPawNdb3rLQA==", 126 | "requires": { 127 | "graphql-extensions": "^0.0.x" 128 | } 129 | }, 130 | "apollo-link": { 131 | "version": "1.2.14", 132 | "resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.14.tgz", 133 | "integrity": "sha512-p67CMEFP7kOG1JZ0ZkYZwRDa369w5PIjtMjvrQd/HnIV8FRsHRqLqK+oAZQnFa1DDdZtOtHTi+aMIW6EatC2jg==", 134 | "requires": { 135 | "apollo-utilities": "^1.3.0", 136 | "ts-invariant": "^0.4.0", 137 | "tslib": "^1.9.3", 138 | "zen-observable-ts": "^0.8.21" 139 | } 140 | }, 141 | "apollo-server-core": { 142 | "version": "1.4.0", 143 | "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-1.4.0.tgz", 144 | "integrity": "sha512-BP1Vh39krgEjkQxbjTdBURUjLHbFq1zeOChDJgaRsMxGtlhzuLWwwC6lLdPatN8jEPbeHq8Tndp9QZ3iQZOKKA==", 145 | "requires": { 146 | "apollo-cache-control": "^0.1.0", 147 | "apollo-tracing": "^0.1.0", 148 | "graphql-extensions": "^0.0.x" 149 | } 150 | }, 151 | "apollo-server-express": { 152 | "version": "1.4.0", 153 | "resolved": "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-1.4.0.tgz", 154 | "integrity": "sha512-zkH00nxhLnJfO0HgnNPBTfZw8qI5ILaPZ5TecMCI9+Y9Ssr2b0bFr9pBRsXy9eudPhI+/O4yqegSUsnLdF/CPw==", 155 | "requires": { 156 | "apollo-server-core": "^1.4.0", 157 | "apollo-server-module-graphiql": "^1.4.0" 158 | } 159 | }, 160 | "apollo-server-lambda": { 161 | "version": "1.3.6", 162 | "resolved": "https://registry.npmjs.org/apollo-server-lambda/-/apollo-server-lambda-1.3.6.tgz", 163 | "integrity": "sha1-varDfxQ8Z5jkC4rnVYC6ZzzqJg4=", 164 | "requires": { 165 | "apollo-server-core": "^1.3.6", 166 | "apollo-server-module-graphiql": "^1.3.4" 167 | } 168 | }, 169 | "apollo-server-module-graphiql": { 170 | "version": "1.4.0", 171 | "resolved": "https://registry.npmjs.org/apollo-server-module-graphiql/-/apollo-server-module-graphiql-1.4.0.tgz", 172 | "integrity": "sha512-GmkOcb5he2x5gat+TuiTvabnBf1m4jzdecal3XbXBh/Jg+kx4hcvO3TTDFQ9CuTprtzdcVyA11iqG7iOMOt7vA==" 173 | }, 174 | "apollo-tracing": { 175 | "version": "0.1.4", 176 | "resolved": "https://registry.npmjs.org/apollo-tracing/-/apollo-tracing-0.1.4.tgz", 177 | "integrity": "sha512-Uv+1nh5AsNmC3m130i2u3IqbS+nrxyVV3KYimH5QKsdPjxxIQB3JAT+jJmpeDxBel8gDVstNmCh82QSLxLSIdQ==", 178 | "requires": { 179 | "graphql-extensions": "~0.0.9" 180 | } 181 | }, 182 | "apollo-upload-server": { 183 | "version": "7.1.0", 184 | "resolved": "https://registry.npmjs.org/apollo-upload-server/-/apollo-upload-server-7.1.0.tgz", 185 | "integrity": "sha512-cD9ReCeyurYwZyEDqJYb5TOc9dt8yhPzS+MtrY3iJdqw+pqiiyPngAvVXHjN+Ca7Lajvom4/AT/PBrYVDMM3Kw==", 186 | "requires": { 187 | "busboy": "^0.2.14", 188 | "fs-capacitor": "^1.0.0", 189 | "http-errors": "^1.7.0", 190 | "object-path": "^0.11.4" 191 | } 192 | }, 193 | "apollo-utilities": { 194 | "version": "1.3.4", 195 | "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.3.4.tgz", 196 | "integrity": "sha512-pk2hiWrCXMAy2fRPwEyhvka+mqwzeP60Jr1tRYi5xru+3ko94HI9o6lK0CT33/w4RDlxWchmdhDCrvdr+pHCig==", 197 | "requires": { 198 | "@wry/equality": "^0.1.2", 199 | "fast-json-stable-stringify": "^2.0.0", 200 | "ts-invariant": "^0.4.0", 201 | "tslib": "^1.10.0" 202 | } 203 | }, 204 | "arangojs": { 205 | "version": "6.14.1", 206 | "resolved": "https://registry.npmjs.org/arangojs/-/arangojs-6.14.1.tgz", 207 | "integrity": "sha512-TJfqwLCo4RyXH5j3i491xKc6qBUsOhd3aIwrTMTuhMkzT6pGRYLvemrmM+XG5HlwYS33M0Ppdj3V6YBsk0HYYg==", 208 | "requires": { 209 | "@types/node": "*", 210 | "es6-error": "^4.0.1", 211 | "multi-part": "^2.0.0", 212 | "x3-linkedlist": "1.0.0", 213 | "xhr": "^2.4.1" 214 | } 215 | }, 216 | "array-flatten": { 217 | "version": "1.1.1", 218 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 219 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 220 | }, 221 | "async-limiter": { 222 | "version": "1.0.1", 223 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", 224 | "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" 225 | }, 226 | "backo2": { 227 | "version": "1.0.2", 228 | "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", 229 | "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" 230 | }, 231 | "body-parser": { 232 | "version": "1.19.0", 233 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", 234 | "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", 235 | "requires": { 236 | "bytes": "3.1.0", 237 | "content-type": "~1.0.4", 238 | "debug": "2.6.9", 239 | "depd": "~1.1.2", 240 | "http-errors": "1.7.2", 241 | "iconv-lite": "0.4.24", 242 | "on-finished": "~2.3.0", 243 | "qs": "6.7.0", 244 | "raw-body": "2.4.0", 245 | "type-is": "~1.6.17" 246 | }, 247 | "dependencies": { 248 | "http-errors": { 249 | "version": "1.7.2", 250 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", 251 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", 252 | "requires": { 253 | "depd": "~1.1.2", 254 | "inherits": "2.0.3", 255 | "setprototypeof": "1.1.1", 256 | "statuses": ">= 1.5.0 < 2", 257 | "toidentifier": "1.0.0" 258 | } 259 | }, 260 | "inherits": { 261 | "version": "2.0.3", 262 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 263 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 264 | } 265 | } 266 | }, 267 | "body-parser-graphql": { 268 | "version": "1.1.0", 269 | "resolved": "https://registry.npmjs.org/body-parser-graphql/-/body-parser-graphql-1.1.0.tgz", 270 | "integrity": "sha512-bOBF4n1AnUjcY1SzLeibeIx4XOuYqEkjn/Lm4yKhnN6KedoXMv4hVqgcKHGRnxOMJP64tErqrQU+4cihhpbJXg==", 271 | "requires": { 272 | "body-parser": "^1.18.2" 273 | } 274 | }, 275 | "buffer-from": { 276 | "version": "1.1.1", 277 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 278 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" 279 | }, 280 | "busboy": { 281 | "version": "0.2.14", 282 | "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", 283 | "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", 284 | "requires": { 285 | "dicer": "0.2.5", 286 | "readable-stream": "1.1.x" 287 | } 288 | }, 289 | "bytes": { 290 | "version": "3.1.0", 291 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", 292 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" 293 | }, 294 | "content-disposition": { 295 | "version": "0.5.3", 296 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", 297 | "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", 298 | "requires": { 299 | "safe-buffer": "5.1.2" 300 | } 301 | }, 302 | "content-type": { 303 | "version": "1.0.4", 304 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 305 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 306 | }, 307 | "cookie": { 308 | "version": "0.4.0", 309 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", 310 | "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" 311 | }, 312 | "cookie-signature": { 313 | "version": "1.0.6", 314 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 315 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 316 | }, 317 | "core-js": { 318 | "version": "2.6.11", 319 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", 320 | "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==" 321 | }, 322 | "core-util-is": { 323 | "version": "1.0.2", 324 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 325 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 326 | }, 327 | "cors": { 328 | "version": "2.8.5", 329 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 330 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 331 | "requires": { 332 | "object-assign": "^4", 333 | "vary": "^1" 334 | } 335 | }, 336 | "debug": { 337 | "version": "2.6.9", 338 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 339 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 340 | "requires": { 341 | "ms": "2.0.0" 342 | } 343 | }, 344 | "depd": { 345 | "version": "1.1.2", 346 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 347 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 348 | }, 349 | "deprecated-decorator": { 350 | "version": "0.1.6", 351 | "resolved": "https://registry.npmjs.org/deprecated-decorator/-/deprecated-decorator-0.1.6.tgz", 352 | "integrity": "sha1-AJZjF7ehL+kvPMgx91g68ym4bDc=" 353 | }, 354 | "destroy": { 355 | "version": "1.0.4", 356 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 357 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 358 | }, 359 | "dicer": { 360 | "version": "0.2.5", 361 | "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", 362 | "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", 363 | "requires": { 364 | "readable-stream": "1.1.x", 365 | "streamsearch": "0.1.2" 366 | } 367 | }, 368 | "dom-walk": { 369 | "version": "0.1.2", 370 | "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", 371 | "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" 372 | }, 373 | "ee-first": { 374 | "version": "1.1.1", 375 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 376 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 377 | }, 378 | "encodeurl": { 379 | "version": "1.0.2", 380 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 381 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 382 | }, 383 | "es6-error": { 384 | "version": "4.1.1", 385 | "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", 386 | "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==" 387 | }, 388 | "escape-html": { 389 | "version": "1.0.3", 390 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 391 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 392 | }, 393 | "etag": { 394 | "version": "1.8.1", 395 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 396 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 397 | }, 398 | "eventemitter3": { 399 | "version": "3.1.2", 400 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", 401 | "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" 402 | }, 403 | "express": { 404 | "version": "4.17.1", 405 | "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", 406 | "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", 407 | "requires": { 408 | "accepts": "~1.3.7", 409 | "array-flatten": "1.1.1", 410 | "body-parser": "1.19.0", 411 | "content-disposition": "0.5.3", 412 | "content-type": "~1.0.4", 413 | "cookie": "0.4.0", 414 | "cookie-signature": "1.0.6", 415 | "debug": "2.6.9", 416 | "depd": "~1.1.2", 417 | "encodeurl": "~1.0.2", 418 | "escape-html": "~1.0.3", 419 | "etag": "~1.8.1", 420 | "finalhandler": "~1.1.2", 421 | "fresh": "0.5.2", 422 | "merge-descriptors": "1.0.1", 423 | "methods": "~1.1.2", 424 | "on-finished": "~2.3.0", 425 | "parseurl": "~1.3.3", 426 | "path-to-regexp": "0.1.7", 427 | "proxy-addr": "~2.0.5", 428 | "qs": "6.7.0", 429 | "range-parser": "~1.2.1", 430 | "safe-buffer": "5.1.2", 431 | "send": "0.17.1", 432 | "serve-static": "1.14.1", 433 | "setprototypeof": "1.1.1", 434 | "statuses": "~1.5.0", 435 | "type-is": "~1.6.18", 436 | "utils-merge": "1.0.1", 437 | "vary": "~1.1.2" 438 | } 439 | }, 440 | "fast-json-stable-stringify": { 441 | "version": "2.1.0", 442 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 443 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" 444 | }, 445 | "file-type": { 446 | "version": "4.4.0", 447 | "resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz", 448 | "integrity": "sha1-G2AOX8ofvcboDApwxxyNul95BsU=" 449 | }, 450 | "finalhandler": { 451 | "version": "1.1.2", 452 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", 453 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", 454 | "requires": { 455 | "debug": "2.6.9", 456 | "encodeurl": "~1.0.2", 457 | "escape-html": "~1.0.3", 458 | "on-finished": "~2.3.0", 459 | "parseurl": "~1.3.3", 460 | "statuses": "~1.5.0", 461 | "unpipe": "~1.0.0" 462 | } 463 | }, 464 | "forwarded": { 465 | "version": "0.1.2", 466 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 467 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 468 | }, 469 | "fresh": { 470 | "version": "0.5.2", 471 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 472 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 473 | }, 474 | "fs-capacitor": { 475 | "version": "1.0.1", 476 | "resolved": "https://registry.npmjs.org/fs-capacitor/-/fs-capacitor-1.0.1.tgz", 477 | "integrity": "sha512-XdZK0Q78WP29Vm3FGgJRhRhrBm51PagovzWtW2kJ3Q6cYJbGtZqWSGTSPwvtEkyjIirFd7b8Yes/dpOYjt4RRQ==" 478 | }, 479 | "global": { 480 | "version": "4.3.2", 481 | "resolved": "https://registry.npmjs.org/global/-/global-4.3.2.tgz", 482 | "integrity": "sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=", 483 | "requires": { 484 | "min-document": "^2.19.0", 485 | "process": "~0.5.1" 486 | } 487 | }, 488 | "graphql": { 489 | "version": "file:../node_modules/graphql", 490 | "integrity": "sha1-VTp9VG1SRmPtpJ7W33dXe+MgOuM=", 491 | "requires": { 492 | "iterall": "^1.2.2" 493 | }, 494 | "dependencies": { 495 | "iterall": { 496 | "version": "1.3.0", 497 | "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", 498 | "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==" 499 | } 500 | } 501 | }, 502 | "graphql-deduplicator": { 503 | "version": "2.0.5", 504 | "resolved": "https://registry.npmjs.org/graphql-deduplicator/-/graphql-deduplicator-2.0.5.tgz", 505 | "integrity": "sha512-09yOnKej64C32saDU+jsQvIxDeYBTzDWhUkqE84AlCB6LUYuUktfgubZkOS3VdoFiYwsi2EL5Vc0wqemS9M3lg==" 506 | }, 507 | "graphql-extensions": { 508 | "version": "0.0.10", 509 | "resolved": "https://registry.npmjs.org/graphql-extensions/-/graphql-extensions-0.0.10.tgz", 510 | "integrity": "sha512-TnQueqUDCYzOSrpQb3q1ngDSP2otJSF+9yNLrQGPzkMsvnQ+v6e2d5tl+B35D4y+XpmvVnAn4T3ZK28mkILveA==", 511 | "requires": { 512 | "core-js": "^2.5.3", 513 | "source-map-support": "^0.5.1" 514 | } 515 | }, 516 | "graphql-import": { 517 | "version": "0.7.1", 518 | "resolved": "https://registry.npmjs.org/graphql-import/-/graphql-import-0.7.1.tgz", 519 | "integrity": "sha512-YpwpaPjRUVlw2SN3OPljpWbVRWAhMAyfSba5U47qGMOSsPLi2gYeJtngGpymjm9nk57RFWEpjqwh4+dpYuFAPw==", 520 | "requires": { 521 | "lodash": "^4.17.4", 522 | "resolve-from": "^4.0.0" 523 | } 524 | }, 525 | "graphql-playground-html": { 526 | "version": "1.6.12", 527 | "resolved": "https://registry.npmjs.org/graphql-playground-html/-/graphql-playground-html-1.6.12.tgz", 528 | "integrity": "sha512-yOYFwwSMBL0MwufeL8bkrNDgRE7eF/kTHiwrqn9FiR9KLcNIl1xw9l9a+6yIRZM56JReQOHpbQFXTZn1IuSKRg==" 529 | }, 530 | "graphql-playground-middleware-express": { 531 | "version": "1.7.11", 532 | "resolved": "https://registry.npmjs.org/graphql-playground-middleware-express/-/graphql-playground-middleware-express-1.7.11.tgz", 533 | "integrity": "sha512-sKItB4s3FxqlwCgXdMfwRAfssSoo31bcFsGAAg/HzaZLicY6CDlofKXP8G5iPDerB6NaoAcAaBLutLzl9sd4fQ==", 534 | "requires": { 535 | "graphql-playground-html": "1.6.12" 536 | } 537 | }, 538 | "graphql-playground-middleware-lambda": { 539 | "version": "1.7.12", 540 | "resolved": "https://registry.npmjs.org/graphql-playground-middleware-lambda/-/graphql-playground-middleware-lambda-1.7.12.tgz", 541 | "integrity": "sha512-fJ1Y0Ck5ctmfaQFoWv7vNnVP7We19P3miVmOT85YPrjpzbMYv0wPfxm4Zjt8nnqXr0KU9nGW53tz3K7/Lvzxtw==", 542 | "requires": { 543 | "graphql-playground-html": "1.6.12" 544 | } 545 | }, 546 | "graphql-subscriptions": { 547 | "version": "0.5.8", 548 | "resolved": "https://registry.npmjs.org/graphql-subscriptions/-/graphql-subscriptions-0.5.8.tgz", 549 | "integrity": "sha512-0CaZnXKBw2pwnIbvmVckby5Ge5e2ecmjofhYCdyeACbCly2j3WXDP/pl+s+Dqd2GQFC7y99NB+53jrt55CKxYQ==", 550 | "requires": { 551 | "iterall": "^1.2.1" 552 | } 553 | }, 554 | "graphql-tools": { 555 | "version": "4.0.8", 556 | "resolved": "https://registry.npmjs.org/graphql-tools/-/graphql-tools-4.0.8.tgz", 557 | "integrity": "sha512-MW+ioleBrwhRjalKjYaLQbr+920pHBgy9vM/n47sswtns8+96sRn5M/G+J1eu7IMeKWiN/9p6tmwCHU7552VJg==", 558 | "requires": { 559 | "apollo-link": "^1.2.14", 560 | "apollo-utilities": "^1.0.1", 561 | "deprecated-decorator": "^0.1.6", 562 | "iterall": "^1.1.3", 563 | "uuid": "^3.1.0" 564 | } 565 | }, 566 | "graphql-upload": { 567 | "version": "8.1.0", 568 | "resolved": "https://registry.npmjs.org/graphql-upload/-/graphql-upload-8.1.0.tgz", 569 | "integrity": "sha512-U2OiDI5VxYmzRKw0Z2dmfk0zkqMRaecH9Smh1U277gVgVe9Qn+18xqf4skwr4YJszGIh7iQDZ57+5ygOK9sM/Q==", 570 | "requires": { 571 | "busboy": "^0.3.1", 572 | "fs-capacitor": "^2.0.4", 573 | "http-errors": "^1.7.3", 574 | "object-path": "^0.11.4" 575 | }, 576 | "dependencies": { 577 | "busboy": { 578 | "version": "0.3.1", 579 | "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.3.1.tgz", 580 | "integrity": "sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw==", 581 | "requires": { 582 | "dicer": "0.3.0" 583 | } 584 | }, 585 | "dicer": { 586 | "version": "0.3.0", 587 | "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz", 588 | "integrity": "sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==", 589 | "requires": { 590 | "streamsearch": "0.1.2" 591 | } 592 | }, 593 | "fs-capacitor": { 594 | "version": "2.0.4", 595 | "resolved": "https://registry.npmjs.org/fs-capacitor/-/fs-capacitor-2.0.4.tgz", 596 | "integrity": "sha512-8S4f4WsCryNw2mJJchi46YgB6CR5Ze+4L1h8ewl9tEpL4SJ3ZO+c/bS4BWhB8bK+O3TMqhuZarTitd0S0eh2pA==" 597 | } 598 | } 599 | }, 600 | "graphql-yoga": { 601 | "version": "1.18.3", 602 | "resolved": "https://registry.npmjs.org/graphql-yoga/-/graphql-yoga-1.18.3.tgz", 603 | "integrity": "sha512-tR6JYbwLSBVu0Z8M7BIyt1PHhhexmRwneYM8Ru/g2pixrtsWbelBFAXU7bDPhXrqZ49Zxt2zLJ60x3bLNGo/bQ==", 604 | "requires": { 605 | "@types/aws-lambda": "8.10.13", 606 | "@types/cors": "^2.8.4", 607 | "@types/express": "^4.11.1", 608 | "@types/graphql": "^14.0.0", 609 | "@types/graphql-deduplicator": "^2.0.0", 610 | "@types/zen-observable": "^0.5.3", 611 | "apollo-server-express": "^1.3.6", 612 | "apollo-server-lambda": "1.3.6", 613 | "apollo-upload-server": "^7.0.0", 614 | "body-parser-graphql": "1.1.0", 615 | "cors": "^2.8.4", 616 | "express": "^4.16.3", 617 | "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0", 618 | "graphql-deduplicator": "^2.0.1", 619 | "graphql-import": "^0.7.0", 620 | "graphql-middleware": "4.0.1", 621 | "graphql-playground-middleware-express": "1.7.11", 622 | "graphql-playground-middleware-lambda": "1.7.12", 623 | "graphql-subscriptions": "^0.5.8", 624 | "graphql-tools": "^4.0.0", 625 | "graphql-upload": "^8.0.0", 626 | "subscriptions-transport-ws": "^0.9.8" 627 | }, 628 | "dependencies": { 629 | "graphql": { 630 | "version": "14.6.0", 631 | "resolved": "https://registry.npmjs.org/graphql/-/graphql-14.6.0.tgz", 632 | "integrity": "sha512-VKzfvHEKybTKjQVpTFrA5yUq2S9ihcZvfJAtsDBBCuV6wauPu1xl/f9ehgVf0FcEJJs4vz6ysb/ZMkGigQZseg==", 633 | "requires": { 634 | "iterall": "^1.2.2" 635 | } 636 | }, 637 | "graphql-middleware": { 638 | "version": "4.0.1", 639 | "resolved": "https://registry.npmjs.org/graphql-middleware/-/graphql-middleware-4.0.1.tgz", 640 | "integrity": "sha512-r9r+pcHV4yZW7LAOcjQYTbNY6nR9SrLgpVZKbrtgXxpQW/MUc1N8q3PESciebvp5s0EEUgRchcRjUkyaArCIFw==", 641 | "requires": { 642 | "graphql-tools": "^4.0.5" 643 | } 644 | } 645 | } 646 | }, 647 | "http-errors": { 648 | "version": "1.7.3", 649 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", 650 | "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", 651 | "requires": { 652 | "depd": "~1.1.2", 653 | "inherits": "2.0.4", 654 | "setprototypeof": "1.1.1", 655 | "statuses": ">= 1.5.0 < 2", 656 | "toidentifier": "1.0.0" 657 | } 658 | }, 659 | "iconv-lite": { 660 | "version": "0.4.24", 661 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 662 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 663 | "requires": { 664 | "safer-buffer": ">= 2.1.2 < 3" 665 | } 666 | }, 667 | "inherits": { 668 | "version": "2.0.4", 669 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 670 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 671 | }, 672 | "ipaddr.js": { 673 | "version": "1.9.1", 674 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 675 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" 676 | }, 677 | "is-function": { 678 | "version": "1.0.2", 679 | "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", 680 | "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==" 681 | }, 682 | "isarray": { 683 | "version": "0.0.1", 684 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", 685 | "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" 686 | }, 687 | "iterall": { 688 | "version": "1.3.0", 689 | "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", 690 | "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==" 691 | }, 692 | "lodash": { 693 | "version": "4.17.15", 694 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", 695 | "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" 696 | }, 697 | "media-typer": { 698 | "version": "0.3.0", 699 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 700 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 701 | }, 702 | "merge-descriptors": { 703 | "version": "1.0.1", 704 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 705 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 706 | }, 707 | "methods": { 708 | "version": "1.1.2", 709 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 710 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 711 | }, 712 | "mime": { 713 | "version": "1.6.0", 714 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 715 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 716 | }, 717 | "mime-db": { 718 | "version": "1.44.0", 719 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", 720 | "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" 721 | }, 722 | "mime-kind": { 723 | "version": "2.0.2", 724 | "resolved": "https://registry.npmjs.org/mime-kind/-/mime-kind-2.0.2.tgz", 725 | "integrity": "sha1-WkPVvr3rCCGCIk2dJjIGMp5Xzfg=", 726 | "requires": { 727 | "file-type": "^4.3.0", 728 | "mime-types": "^2.1.15" 729 | } 730 | }, 731 | "mime-types": { 732 | "version": "2.1.27", 733 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", 734 | "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", 735 | "requires": { 736 | "mime-db": "1.44.0" 737 | } 738 | }, 739 | "min-document": { 740 | "version": "2.19.0", 741 | "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", 742 | "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", 743 | "requires": { 744 | "dom-walk": "^0.1.0" 745 | } 746 | }, 747 | "ms": { 748 | "version": "2.0.0", 749 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 750 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 751 | }, 752 | "multi-part": { 753 | "version": "2.0.0", 754 | "resolved": "https://registry.npmjs.org/multi-part/-/multi-part-2.0.0.tgz", 755 | "integrity": "sha1-Z09TtDL4UM+MwC0w0h8gZOMJVjw=", 756 | "requires": { 757 | "mime-kind": "^2.0.1" 758 | } 759 | }, 760 | "negotiator": { 761 | "version": "0.6.2", 762 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", 763 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" 764 | }, 765 | "object-assign": { 766 | "version": "4.1.1", 767 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 768 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 769 | }, 770 | "object-path": { 771 | "version": "0.11.4", 772 | "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.4.tgz", 773 | "integrity": "sha1-NwrnUvvzfePqcKhhwju6iRVpGUk=" 774 | }, 775 | "on-finished": { 776 | "version": "2.3.0", 777 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 778 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 779 | "requires": { 780 | "ee-first": "1.1.1" 781 | } 782 | }, 783 | "parse-headers": { 784 | "version": "2.0.3", 785 | "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.3.tgz", 786 | "integrity": "sha512-QhhZ+DCCit2Coi2vmAKbq5RGTRcQUOE2+REgv8vdyu7MnYx2eZztegqtTx99TZ86GTIwqiy3+4nQTWZ2tgmdCA==" 787 | }, 788 | "parseurl": { 789 | "version": "1.3.3", 790 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 791 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 792 | }, 793 | "path-to-regexp": { 794 | "version": "0.1.7", 795 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 796 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 797 | }, 798 | "process": { 799 | "version": "0.5.2", 800 | "resolved": "https://registry.npmjs.org/process/-/process-0.5.2.tgz", 801 | "integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=" 802 | }, 803 | "proxy-addr": { 804 | "version": "2.0.6", 805 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", 806 | "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", 807 | "requires": { 808 | "forwarded": "~0.1.2", 809 | "ipaddr.js": "1.9.1" 810 | } 811 | }, 812 | "qs": { 813 | "version": "6.7.0", 814 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 815 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" 816 | }, 817 | "range-parser": { 818 | "version": "1.2.1", 819 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 820 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 821 | }, 822 | "raw-body": { 823 | "version": "2.4.0", 824 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", 825 | "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", 826 | "requires": { 827 | "bytes": "3.1.0", 828 | "http-errors": "1.7.2", 829 | "iconv-lite": "0.4.24", 830 | "unpipe": "1.0.0" 831 | }, 832 | "dependencies": { 833 | "http-errors": { 834 | "version": "1.7.2", 835 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", 836 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", 837 | "requires": { 838 | "depd": "~1.1.2", 839 | "inherits": "2.0.3", 840 | "setprototypeof": "1.1.1", 841 | "statuses": ">= 1.5.0 < 2", 842 | "toidentifier": "1.0.0" 843 | } 844 | }, 845 | "inherits": { 846 | "version": "2.0.3", 847 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 848 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 849 | } 850 | } 851 | }, 852 | "readable-stream": { 853 | "version": "1.1.14", 854 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", 855 | "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", 856 | "requires": { 857 | "core-util-is": "~1.0.0", 858 | "inherits": "~2.0.1", 859 | "isarray": "0.0.1", 860 | "string_decoder": "~0.10.x" 861 | } 862 | }, 863 | "resolve-from": { 864 | "version": "4.0.0", 865 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 866 | "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" 867 | }, 868 | "safe-buffer": { 869 | "version": "5.1.2", 870 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 871 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 872 | }, 873 | "safer-buffer": { 874 | "version": "2.1.2", 875 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 876 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 877 | }, 878 | "send": { 879 | "version": "0.17.1", 880 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", 881 | "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", 882 | "requires": { 883 | "debug": "2.6.9", 884 | "depd": "~1.1.2", 885 | "destroy": "~1.0.4", 886 | "encodeurl": "~1.0.2", 887 | "escape-html": "~1.0.3", 888 | "etag": "~1.8.1", 889 | "fresh": "0.5.2", 890 | "http-errors": "~1.7.2", 891 | "mime": "1.6.0", 892 | "ms": "2.1.1", 893 | "on-finished": "~2.3.0", 894 | "range-parser": "~1.2.1", 895 | "statuses": "~1.5.0" 896 | }, 897 | "dependencies": { 898 | "ms": { 899 | "version": "2.1.1", 900 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 901 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 902 | } 903 | } 904 | }, 905 | "serve-static": { 906 | "version": "1.14.1", 907 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", 908 | "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", 909 | "requires": { 910 | "encodeurl": "~1.0.2", 911 | "escape-html": "~1.0.3", 912 | "parseurl": "~1.3.3", 913 | "send": "0.17.1" 914 | } 915 | }, 916 | "setprototypeof": { 917 | "version": "1.1.1", 918 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 919 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 920 | }, 921 | "source-map": { 922 | "version": "0.6.1", 923 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 924 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" 925 | }, 926 | "source-map-support": { 927 | "version": "0.5.19", 928 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", 929 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", 930 | "requires": { 931 | "buffer-from": "^1.0.0", 932 | "source-map": "^0.6.0" 933 | } 934 | }, 935 | "statuses": { 936 | "version": "1.5.0", 937 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 938 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 939 | }, 940 | "streamsearch": { 941 | "version": "0.1.2", 942 | "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", 943 | "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" 944 | }, 945 | "string_decoder": { 946 | "version": "0.10.31", 947 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", 948 | "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" 949 | }, 950 | "subscriptions-transport-ws": { 951 | "version": "0.9.16", 952 | "resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.16.tgz", 953 | "integrity": "sha512-pQdoU7nC+EpStXnCfh/+ho0zE0Z+ma+i7xvj7bkXKb1dvYHSZxgRPaU6spRP+Bjzow67c/rRDoix5RT0uU9omw==", 954 | "requires": { 955 | "backo2": "^1.0.2", 956 | "eventemitter3": "^3.1.0", 957 | "iterall": "^1.2.1", 958 | "symbol-observable": "^1.0.4", 959 | "ws": "^5.2.0" 960 | } 961 | }, 962 | "symbol-observable": { 963 | "version": "1.2.0", 964 | "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", 965 | "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" 966 | }, 967 | "toidentifier": { 968 | "version": "1.0.0", 969 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 970 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 971 | }, 972 | "ts-invariant": { 973 | "version": "0.4.4", 974 | "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.4.tgz", 975 | "integrity": "sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==", 976 | "requires": { 977 | "tslib": "^1.9.3" 978 | } 979 | }, 980 | "tslib": { 981 | "version": "1.13.0", 982 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", 983 | "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" 984 | }, 985 | "type-is": { 986 | "version": "1.6.18", 987 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 988 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 989 | "requires": { 990 | "media-typer": "0.3.0", 991 | "mime-types": "~2.1.24" 992 | } 993 | }, 994 | "unpipe": { 995 | "version": "1.0.0", 996 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 997 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 998 | }, 999 | "utils-merge": { 1000 | "version": "1.0.1", 1001 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1002 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 1003 | }, 1004 | "uuid": { 1005 | "version": "3.4.0", 1006 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", 1007 | "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" 1008 | }, 1009 | "vary": { 1010 | "version": "1.1.2", 1011 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1012 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 1013 | }, 1014 | "ws": { 1015 | "version": "5.2.2", 1016 | "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", 1017 | "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", 1018 | "requires": { 1019 | "async-limiter": "~1.0.0" 1020 | } 1021 | }, 1022 | "x3-linkedlist": { 1023 | "version": "1.0.0", 1024 | "resolved": "https://registry.npmjs.org/x3-linkedlist/-/x3-linkedlist-1.0.0.tgz", 1025 | "integrity": "sha512-8CwA4XCMtso4G6qJWCzqbWQ9YJjtRiD4rUHFJ77rlAXQUN38Ni9E84y4F9qt4ijxZhfpJVm9tRs8E2vdLC4ZqQ==" 1026 | }, 1027 | "xhr": { 1028 | "version": "2.5.0", 1029 | "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.5.0.tgz", 1030 | "integrity": "sha512-4nlO/14t3BNUZRXIXfXe+3N6w3s1KoxcJUUURctd64BLRe67E4gRwp4PjywtDY72fXpZ1y6Ch0VZQRY/gMPzzQ==", 1031 | "requires": { 1032 | "global": "~4.3.0", 1033 | "is-function": "^1.0.1", 1034 | "parse-headers": "^2.0.0", 1035 | "xtend": "^4.0.0" 1036 | } 1037 | }, 1038 | "xtend": { 1039 | "version": "4.0.2", 1040 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 1041 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" 1042 | }, 1043 | "zen-observable": { 1044 | "version": "0.8.15", 1045 | "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", 1046 | "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" 1047 | }, 1048 | "zen-observable-ts": { 1049 | "version": "0.8.21", 1050 | "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.21.tgz", 1051 | "integrity": "sha512-Yj3yXweRc8LdRMrCC8nIc4kkjWecPAUVh0TI0OUrWXx6aX790vLcDlWca6I4vsyCGH3LpWxq0dJRcMOFoVqmeg==", 1052 | "requires": { 1053 | "tslib": "^1.9.3", 1054 | "zen-observable": "^0.8.0" 1055 | } 1056 | } 1057 | } 1058 | } 1059 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ./server.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "arangojs": "^6.14.1", 14 | "graphql": "../node_modules/graphql", 15 | "graphql-yoga": "^1.18.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | // NOTE - in your code, this would be importing from graphql-arangodb 2 | const { directiveTypeDefs, resolver: aqlResolver } = require('../dist'); 3 | const { GraphQLServer } = require('graphql-yoga'); 4 | const { Database } = require('arangojs'); 5 | 6 | const typeDefs = ` 7 | ${directiveTypeDefs} 8 | 9 | type User { 10 | id: ID! @aqlKey 11 | name: String! 12 | bio: String 13 | 14 | posts: [Post!]! 15 | @aqlNode( 16 | edgeCollection: "posted" 17 | direction: OUTBOUND 18 | # only show published posts for users 19 | filter: "$field.publishedAt != null" 20 | ) 21 | 22 | # authenticated users can see their own drafts. for this subquery 23 | # we get the authenticated user using a context value of their id, 24 | # then if they equal the parent node we return drafts - otherwise 25 | # nothing. 26 | drafts: [Post!] 27 | @aqlSubquery( 28 | query: """ 29 | LET authenticatedUser = DOCUMENT('users', $context.userId) 30 | LET allAuthorizedDrafts = 31 | authenticatedUser == $parent 32 | ? (FOR post IN OUTBOUND authenticatedUser posted FILTER post.publishedAt == null RETURN post) 33 | : null 34 | 35 | """ 36 | return: "allAuthorizedDrafts" 37 | ) 38 | 39 | friendships(first: Int = 10): [Friendship!]! 40 | @aqlEdge( 41 | collection: "friendOf" 42 | # inbound or outbound edges 43 | direction: ANY 44 | # sort by the friend User's name 45 | sort: { property: "name", sortOn: "$field_node" } 46 | # limit based on the passed argument 47 | limit: "$args.first" 48 | ) 49 | } 50 | 51 | type Post { 52 | id: ID! @aqlKey 53 | title: String! 54 | body: String! 55 | publishedAt: String! 56 | author: User! 57 | @aqlNode(edgeCollection: "posted", direction: INBOUND) 58 | } 59 | 60 | type Friendship { 61 | strength: Int 62 | user: User! @aqlEdgeNode 63 | } 64 | 65 | type Query { 66 | user(id: ID!): User 67 | @aqlDocument(collection: "users", key: "$args.id") 68 | 69 | users: [User!]! 70 | @aqlDocument(collection: "users") 71 | 72 | posts: [Post!]! 73 | @aqlDocument(collection: "posts") 74 | } 75 | 76 | type CreatePostPayload { 77 | post: Post! 78 | @aqlNewQuery 79 | @aqlSubquery( 80 | query: """ 81 | LET $field = DOCUMENT(posts, $parent.post._key) 82 | """ 83 | ) 84 | } 85 | 86 | type CreateUserPayload { 87 | user: User! 88 | @aqlNewQuery 89 | @aqlSubquery( 90 | query: """ 91 | LET $field = DOCUMENT(users, $parent.user._key) 92 | """ 93 | ) 94 | } 95 | 96 | type Mutation { 97 | createPost(title: String!, body: String!): CreatePostPayload! 98 | @aqlSubquery( 99 | query: """ 100 | LET user = DOCUMENT(users, $context.userId) 101 | LET post = FIRST( 102 | INSERT { title: $args.title, body: $args.body } 103 | INTO posts 104 | RETURN NEW 105 | ) 106 | INSERT { _from: user._id, _to: post._id } INTO posted 107 | """ 108 | return: "{ post: post }" 109 | ) 110 | 111 | createExampleUser: CreateUserPayload! 112 | @aqlSubquery( 113 | query: """ 114 | INSERT { name: "Example", _key: "exampleKey", bio: "I exist" } 115 | INTO users 116 | """ 117 | return: "{ user: NEW }" 118 | ) 119 | } 120 | `; 121 | 122 | // IMPORTANT - add resolvers for every 'top-level' AQL operation. 123 | // That's basically anything under Query, Mutation, and anything marked 124 | // with @aqlNewQuery 125 | const resolvers = { 126 | Query: { 127 | user: aqlResolver, 128 | users: aqlResolver, 129 | posts: aqlResolver, 130 | }, 131 | Mutation: { 132 | createPost: aqlResolver, 133 | createExampleUser: aqlResolver, 134 | }, 135 | // important: because we split the query in this type, 136 | // a resolver is required 137 | CreatePostPayload: { 138 | post: aqlResolver, 139 | }, 140 | CreateUserPayload: { 141 | user: aqlResolver, 142 | }, 143 | }; 144 | 145 | const arangoDb = new Database({ 146 | url: 'http://localhost:8529', 147 | }); 148 | arangoDb.useDatabase('exampleDb'); 149 | arangoDb.useBasicAuth('username', 'password'); 150 | 151 | const context = { 152 | arangoDb, 153 | // in a real app, you'd define your context as a function which 154 | // might check cookies or a JWT to determine the identity of 155 | // the requesting user. For this demo, we use the hardcoded 156 | // key we specify in the createExampleUser operation above. 157 | arangoContext: { 158 | userId: 'exampleKey', 159 | }, 160 | }; 161 | 162 | const server = new GraphQLServer({ 163 | typeDefs, 164 | resolvers, 165 | context, 166 | }); 167 | 168 | server.start(() => console.log('Server is running on localhost:4000')); 169 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '.(ts|tsx)': require.resolve('ts-jest/dist'), 4 | }, 5 | transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$'], 6 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 7 | collectCoverageFrom: ['src/**/*.{ts,tsx}'], 8 | testRegex: '(src/.*__tests__/.*\\.(test|spec))\\.(ts|tsx|js)$', 9 | testURL: 'http://localhost', 10 | setupFiles: ['/src/__tests__/config/setup.ts'], 11 | resetMocks: true, 12 | }; 13 | -------------------------------------------------------------------------------- /jest.integration.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '.(ts|tsx)': require.resolve('ts-jest/dist'), 4 | }, 5 | testEnvironment: 'node', 6 | testRegex: '(src/.*__integration_tests__/.*\\.(test|spec))\\.(ts|tsx|js)$', 7 | moduleFileExtensions: ['ts', 'tsx', 'js'], 8 | setupFiles: ['/src/__tests__/config/setup.ts'], 9 | }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-arangodb", 3 | "version": "0.1.24", 4 | "license": "MIT", 5 | "main": "dist/index.js", 6 | "module": "dist/graphql-arangodb.esm.js", 7 | "typings": "dist/index.d.ts", 8 | "files": [ 9 | "dist" 10 | ], 11 | "scripts": { 12 | "start": "tsdx watch", 13 | "build": "tsdx build", 14 | "test": "jest", 15 | "prepublishOnly": "npm run build" 16 | }, 17 | "peerDependencies": { 18 | "arangojs": "^6.10.0", 19 | "graphql": "^14.4.2" 20 | }, 21 | "husky": { 22 | "hooks": { 23 | "pre-commit": "pretty-quick --staged" 24 | } 25 | }, 26 | "prettier": { 27 | "printWidth": 80, 28 | "semi": true, 29 | "singleQuote": true, 30 | "trailingComma": "es5" 31 | }, 32 | "devDependencies": { 33 | "@types/graphql": "^14.2.2", 34 | "@types/jest": "^24.0.15", 35 | "@types/ramda": "^0.26.16", 36 | "arangojs": "^6.10.0", 37 | "graphql": "^14.4.2", 38 | "husky": "^3.0.0", 39 | "prettier": "^1.18.2", 40 | "pretty-quick": "^1.11.1", 41 | "tsdx": "^0.7.2", 42 | "tslib": "^1.10.0", 43 | "typescript": "^3.5.3" 44 | }, 45 | "dependencies": { 46 | "graphql-middleware": "^3.0.2", 47 | "graphql-tools": "^4.0.5", 48 | "ramda": "^0.26.1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/__tests__/config/setup.ts: -------------------------------------------------------------------------------- 1 | (global as any)['__DEV__'] = true; 2 | -------------------------------------------------------------------------------- /src/__tests__/fixtures/typeDefs.ts: -------------------------------------------------------------------------------- 1 | import { directiveTypeDefs } from '../../typeDefs'; 2 | 3 | export default ` 4 | ${directiveTypeDefs} 5 | 6 | type User { 7 | id: ID! @aqlKey 8 | name: String! 9 | bio: String 10 | 11 | fullName: String! @aql(expression: "CONCAT($parent.name, \\" \\", $parent.surname)") 12 | 13 | simplePosts: [Post!]! 14 | @aqlNode( 15 | edgeCollection: "posted" 16 | direction: OUTBOUND 17 | ) 18 | 19 | filteredPosts(titleMatch: String): [Post!]! 20 | @aqlNode( 21 | edgeCollection: "posted" 22 | direction: OUTBOUND 23 | filter: "$field.title =~ $args.titleMatch" 24 | ) 25 | 26 | paginatedPosts(count: Int!, sort: String = "title", skip: Int = 0): [Post!]! 27 | @aqlNode( 28 | edgeCollection: "posted" 29 | direction: OUTBOUND 30 | sort: { 31 | property: "$args.sort" 32 | } 33 | limit: { 34 | skip: "$args.skip" 35 | count: "$args.count" 36 | } 37 | ) 38 | 39 | descendingPosts: [Post!]! 40 | @aqlNode( 41 | edgeCollection: "posted" 42 | direction: OUTBOUND 43 | sort: { 44 | property: "title" 45 | order: DESC 46 | } 47 | ) 48 | 49 | bfsPosts: [Post!]! 50 | @aqlNode( 51 | edgeCollection: "posted" 52 | direction: OUTBOUND 53 | options: { 54 | bfs: true 55 | } 56 | ) 57 | 58 | friends: [FriendOfEdge!]! 59 | @aqlEdge( 60 | collection: "friendOf" 61 | direction: ANY 62 | ) 63 | 64 | friendsOfFriends: [User!]! 65 | @aqlSubquery( 66 | query: """ 67 | FOR $field IN 2..2 ANY $parent friendOf OPTIONS {bfs: true, uniqueVertices: 'path'} 68 | """ 69 | ) 70 | 71 | postsConnection(first: Int = 10, after: String!, filter: PostsConnectionFilter): UserPostsConnection! 72 | @aqlRelayConnection( 73 | edgeCollection: "posted" 74 | edgeDirection: OUTBOUND 75 | cursorExpression: "$node.title" 76 | filter: """ 77 | ( 78 | $args['filter'] != null && ( 79 | $args['filter'].publishedAfter == null || $node.publishedAt > $args['filter'].publishedAfter 80 | ) && ( 81 | $args['filter'].titleLike == null || LIKE($node.title, CONCAT("%", $args['filter'].titleLike, "%")) 82 | ) 83 | ) 84 | """ 85 | ) 86 | } 87 | 88 | input PostsConnectionFilter { 89 | publishedAfter: String 90 | titleLike: String 91 | } 92 | 93 | type Post { 94 | id: ID! @aqlKey 95 | title: String! 96 | body: String! 97 | publishedAt: String! 98 | author: User! 99 | @aqlNode(edgeCollection: "posted", direction: INBOUND) 100 | } 101 | 102 | type FriendOfEdge { 103 | strength: Int 104 | user: User! @aqlEdgeNode 105 | } 106 | 107 | type UserPostsConnection { 108 | edges: [UserPostEdge!]! @aqlRelayEdges 109 | pageInfo: UserPostsPageInfo! @aqlRelayPageInfo 110 | } 111 | 112 | type UserPostEdge { 113 | cursor: String! 114 | node: Post! @aqlRelayNode 115 | } 116 | 117 | type UserPostsPageInfo { 118 | hasNextPage: Boolean 119 | } 120 | 121 | type PostsConnection { 122 | edges: [PostEdge!]! @aqlRelayEdges 123 | pageInfo: PostsPageInfo! @aqlRelayPageInfo 124 | } 125 | 126 | type PostEdge { 127 | cursor: String! 128 | node: Post! @aqlRelayNode 129 | } 130 | 131 | type PostsPageInfo { 132 | hasNextPage: Boolean! 133 | endCursor: String 134 | } 135 | 136 | type Query { 137 | user(id: ID!): User 138 | @aqlDocument( 139 | collection: "users" 140 | key: "$args.id" 141 | ) 142 | 143 | users: [User!]! 144 | @aqlDocument( 145 | collection: "users" 146 | ) 147 | 148 | posts(first: Int = 10, after: String, searchTerm: String): PostsConnection! 149 | 150 | authorizedPosts: [Post!]! 151 | @aqlSubquery( 152 | query: """ 153 | LET authenticatedUser = DOCUMENT('users', $context.userId) 154 | LET allAuthorizedPosts = UNION_DISTINCT( 155 | (FOR post IN posts FILTER post.public == true RETURN post), 156 | (FOR post IN OUTBOUND authenticatedUser posted RETURN post) 157 | ) 158 | """ 159 | return: "allAuthorizedPosts" 160 | ) 161 | } 162 | 163 | type CreatePostPayload { 164 | post: Post! 165 | @aqlNewQuery 166 | @aqlSubquery( 167 | query: """ 168 | LET $field = DOCUMENT(posts, $parent._key) 169 | """ 170 | ) 171 | } 172 | 173 | type Mutation { 174 | """This tests custom resolver query support""" 175 | createUser: User! 176 | 177 | """Tests multi-query resolution to avoid 'read after write' errors""" 178 | createPost: CreatePostPayload! 179 | @aqlSubquery( 180 | query: """ 181 | INSERT { title: "Fake post", body: "foo", publishedAt: "2019-05-03" } 182 | INTO posts 183 | OPTIONS { waitForSync: true } 184 | LET $field = { 185 | post: NEW 186 | } 187 | """ 188 | ) 189 | } 190 | `; 191 | -------------------------------------------------------------------------------- /src/__tests__/queryTranslation.test.ts: -------------------------------------------------------------------------------- 1 | import { makeExecutableSchema } from 'graphql-tools'; 2 | import typeDefs from './fixtures/typeDefs'; 3 | import { Database, aql } from 'arangojs'; 4 | import { graphql } from 'graphql'; 5 | import aqlResolver, { builders } from '..'; 6 | 7 | describe('query translation integration tests', () => { 8 | const schema = makeExecutableSchema({ 9 | typeDefs, 10 | resolvers: { 11 | Query: { 12 | user: aqlResolver, 13 | users: aqlResolver, 14 | authorizedPosts: aqlResolver, 15 | posts: async (parent, args, context, info) => { 16 | if (args.searchTerm) { 17 | return aqlResolver.runCustomQuery({ 18 | queryBuilder: builders.aqlRelayConnection({ 19 | // this sets up the relay connection to draw from a search view using the requested search term 20 | source: `FOR $node IN SearchView SEARCH PHRASE($node.name, $args.searchTerm, 'text_en')`, 21 | // our 'cursor' will actually be the weight value of the result, allowing proper sorting of results by weight. 22 | cursorExpression: `BM25($node)`, 23 | // because we order by weight, we actually want to start at higher values and go down 24 | sortOrder: 'DESC', 25 | }), 26 | parent, 27 | args, 28 | context, 29 | info, 30 | }); 31 | } else { 32 | return aqlResolver.runCustomQuery({ 33 | queryBuilder: builders.aqlRelayConnection({ 34 | source: `FOR $node IN posts`, 35 | cursorExpression: '$node.createdAt', 36 | }), 37 | parent, 38 | args, 39 | context, 40 | info, 41 | }); 42 | } 43 | }, 44 | }, 45 | Mutation: { 46 | createUser: async (parent: any, args: any, ctx: any, info: any) => { 47 | const bindVars = { 48 | userId: 'foobar', 49 | role: 'captain', 50 | name: 'Bob', 51 | }; 52 | 53 | return aqlResolver.runCustomQuery({ 54 | query: aql` 55 | INSERT {_key: ${bindVars.userId}, role: ${bindVars.role}, name: ${bindVars.name}} INTO users 56 | RETURN NEW 57 | `, 58 | parent, 59 | args, 60 | context: ctx, 61 | info, 62 | }); 63 | }, 64 | createPost: aqlResolver, 65 | }, 66 | CreatePostPayload: { 67 | post: aqlResolver, 68 | }, 69 | }, 70 | }); 71 | 72 | const mockRunQuery = jest.fn(); 73 | 74 | const mockDb = ({ 75 | query: mockRunQuery, 76 | } as any) as Database; 77 | 78 | const run = async ( 79 | query: string, 80 | mockResults: any[], 81 | contextValue: any = {} 82 | ) => { 83 | mockResults.forEach(mockResult => { 84 | mockRunQuery.mockResolvedValueOnce({ 85 | all: () => Promise.resolve([mockResult]), 86 | }); 87 | }); 88 | 89 | const result = await graphql({ 90 | schema, 91 | source: query, 92 | contextValue: { 93 | arangoContext: contextValue, 94 | arangoDb: mockDb, 95 | }, 96 | }); 97 | 98 | if (result.errors) { 99 | throw result.errors[0]; 100 | } 101 | 102 | return result.data; 103 | }; 104 | 105 | test('translates a basic document query', async () => { 106 | await run( 107 | ` 108 | query GetUser { 109 | user(id: "foo") { 110 | id 111 | name 112 | bio 113 | } 114 | } 115 | `, 116 | [ 117 | { 118 | id: 'foo', 119 | name: 'Foo', 120 | bio: 'No thanks', 121 | }, 122 | ] 123 | ); 124 | 125 | expect(mockRunQuery.mock.calls[0][0].query).toMatchInlineSnapshot(` 126 | "LET query = FIRST( 127 | LET user = DOCUMENT(users, @field_user.args.id) 128 | RETURN { 129 | _id: user._id, 130 | _key: user._key, 131 | _rev: user._rev, 132 | name: user.name, 133 | bio: user.bio, 134 | id: user._key 135 | } 136 | ) 137 | RETURN query" 138 | `); 139 | expect(mockRunQuery.mock.calls[0][0].bindVars).toMatchInlineSnapshot(` 140 | Object { 141 | "field_user": Object { 142 | "args": Object { 143 | "id": "foo", 144 | }, 145 | }, 146 | } 147 | `); 148 | }); 149 | 150 | test('translates a document with a nested node', async () => { 151 | await run( 152 | ` 153 | query GetUserAndPosts { 154 | user(id: "foo") { 155 | id 156 | name 157 | 158 | simplePosts { 159 | id 160 | title 161 | } 162 | } 163 | } 164 | `, 165 | [ 166 | { 167 | id: 'foo', 168 | name: 'Foo', 169 | simplePosts: [ 170 | { 171 | id: 'a', 172 | title: 'Hello world', 173 | }, 174 | { 175 | id: 'b', 176 | title: 'Hello again world', 177 | }, 178 | { 179 | id: 'c', 180 | title: 'Come here often, world?', 181 | }, 182 | ], 183 | }, 184 | ] 185 | ); 186 | 187 | expect(mockRunQuery.mock.calls[0][0].query).toMatchInlineSnapshot(` 188 | "LET query = FIRST( 189 | LET user = DOCUMENT(users, @field_user.args.id) 190 | RETURN { 191 | _id: user._id, 192 | _key: user._key, 193 | _rev: user._rev, 194 | name: user.name, 195 | id: user._key, 196 | simplePosts: ( 197 | FOR user_simplePosts IN OUTBOUND user posted 198 | RETURN { 199 | _id: user_simplePosts._id, 200 | _key: user_simplePosts._key, 201 | _rev: user_simplePosts._rev, 202 | title: user_simplePosts.title, 203 | id: user_simplePosts._key 204 | } 205 | ) 206 | } 207 | ) 208 | RETURN query" 209 | `); 210 | expect(mockRunQuery.mock.calls[0][0].bindVars).toMatchInlineSnapshot(` 211 | Object { 212 | "field_user": Object { 213 | "args": Object { 214 | "id": "foo", 215 | }, 216 | }, 217 | } 218 | `); 219 | }); 220 | 221 | test('filters', async () => { 222 | await run( 223 | ` 224 | query GetUserAndFilteredPosts { 225 | user(id: "foo") { 226 | id 227 | 228 | filteredPosts(titleMatch: "here") { 229 | id 230 | title 231 | } 232 | } 233 | } 234 | `, 235 | [ 236 | { 237 | id: 'foo', 238 | filteredPosts: [ 239 | { 240 | id: 'c', 241 | title: 'Come here often, world?', 242 | }, 243 | ], 244 | }, 245 | ] 246 | ); 247 | 248 | expect(mockRunQuery.mock.calls[0][0].query).toMatchInlineSnapshot(` 249 | "LET query = FIRST( 250 | LET user = DOCUMENT(users, @field_user.args.id) 251 | RETURN { 252 | _id: user._id, 253 | _key: user._key, 254 | _rev: user._rev, 255 | id: user._key, 256 | filteredPosts: ( 257 | FOR user_filteredPosts IN OUTBOUND user posted 258 | FILTER user_filteredPosts.title =~ @field_user_filteredPosts.args.titleMatch 259 | RETURN { 260 | _id: user_filteredPosts._id, 261 | _key: user_filteredPosts._key, 262 | _rev: user_filteredPosts._rev, 263 | title: user_filteredPosts.title, 264 | id: user_filteredPosts._key 265 | } 266 | ) 267 | } 268 | ) 269 | RETURN query" 270 | `); 271 | expect(mockRunQuery.mock.calls[0][0].bindVars).toMatchInlineSnapshot(` 272 | Object { 273 | "field_user": Object { 274 | "args": Object { 275 | "id": "foo", 276 | }, 277 | }, 278 | "field_user_filteredPosts": Object { 279 | "args": Object { 280 | "titleMatch": "here", 281 | }, 282 | }, 283 | } 284 | `); 285 | }); 286 | 287 | test('paginates', async () => { 288 | await run( 289 | ` 290 | query GetUserAndPaginatedPosts { 291 | user(id: "foo") { 292 | id 293 | 294 | paginatedPosts(count: 2) { 295 | id 296 | title 297 | } 298 | } 299 | } 300 | `, 301 | [ 302 | { 303 | id: 'foo', 304 | paginatedPosts: [ 305 | { 306 | id: 'b', 307 | title: 'Hello again world', 308 | }, 309 | { 310 | id: 'c', 311 | title: 'Come here often, world?', 312 | }, 313 | ], 314 | }, 315 | ] 316 | ); 317 | 318 | expect(mockRunQuery.mock.calls[0][0].query).toMatchInlineSnapshot(` 319 | "LET query = FIRST( 320 | LET user = DOCUMENT(users, @field_user.args.id) 321 | RETURN { 322 | _id: user._id, 323 | _key: user._key, 324 | _rev: user._rev, 325 | id: user._key, 326 | paginatedPosts: ( 327 | FOR user_paginatedPosts IN OUTBOUND user posted 328 | SORT user_paginatedPosts[@field_user_paginatedPosts.args.sort] ASC 329 | LIMIT @field_user_paginatedPosts.args.skip @field_user_paginatedPosts.args.count 330 | RETURN { 331 | _id: user_paginatedPosts._id, 332 | _key: user_paginatedPosts._key, 333 | _rev: user_paginatedPosts._rev, 334 | title: user_paginatedPosts.title, 335 | id: user_paginatedPosts._key 336 | } 337 | ) 338 | } 339 | ) 340 | RETURN query" 341 | `); 342 | expect(mockRunQuery.mock.calls[0][0].bindVars).toMatchInlineSnapshot(` 343 | Object { 344 | "field_user": Object { 345 | "args": Object { 346 | "id": "foo", 347 | }, 348 | }, 349 | "field_user_paginatedPosts": Object { 350 | "args": Object { 351 | "count": 2, 352 | "skip": 0, 353 | "sort": "title", 354 | }, 355 | }, 356 | } 357 | `); 358 | }); 359 | 360 | test('sorts descending', async () => { 361 | await run( 362 | ` 363 | query GetUserAndDescendingPosts { 364 | user(id: "foo") { 365 | id 366 | 367 | descendingPosts { 368 | id 369 | title 370 | } 371 | } 372 | } 373 | `, 374 | [ 375 | { 376 | id: 'foo', 377 | descendingPosts: [ 378 | { 379 | id: 'b', 380 | title: 'Hello again world', 381 | }, 382 | { 383 | id: 'c', 384 | title: 'Come here often, world?', 385 | }, 386 | ], 387 | }, 388 | ] 389 | ); 390 | 391 | expect(mockRunQuery.mock.calls[0][0].query).toMatchInlineSnapshot(` 392 | "LET query = FIRST( 393 | LET user = DOCUMENT(users, @field_user.args.id) 394 | RETURN { 395 | _id: user._id, 396 | _key: user._key, 397 | _rev: user._rev, 398 | id: user._key, 399 | descendingPosts: ( 400 | FOR user_descendingPosts IN OUTBOUND user posted 401 | SORT user_descendingPosts[\\"title\\"] DESC 402 | RETURN { 403 | _id: user_descendingPosts._id, 404 | _key: user_descendingPosts._key, 405 | _rev: user_descendingPosts._rev, 406 | title: user_descendingPosts.title, 407 | id: user_descendingPosts._key 408 | } 409 | ) 410 | } 411 | ) 412 | RETURN query" 413 | `); 414 | expect(mockRunQuery.mock.calls[0][0].bindVars).toMatchInlineSnapshot(` 415 | Object { 416 | "field_user": Object { 417 | "args": Object { 418 | "id": "foo", 419 | }, 420 | }, 421 | } 422 | `); 423 | }); 424 | 425 | test('traverses edges', async () => { 426 | await run( 427 | ` 428 | query GetUserAndFriends { 429 | user(id: "foo") { 430 | id 431 | name 432 | 433 | friends { 434 | strength 435 | 436 | user { 437 | id 438 | name 439 | } 440 | } 441 | } 442 | } 443 | `, 444 | [ 445 | { 446 | id: 'foo', 447 | name: 'Bar', 448 | friends: [ 449 | { 450 | strength: 2, 451 | user: { 452 | id: 'bar', 453 | name: 'Jeff', 454 | }, 455 | }, 456 | ], 457 | }, 458 | ] 459 | ); 460 | 461 | expect(mockRunQuery.mock.calls[0][0].query).toMatchInlineSnapshot(` 462 | "LET query = FIRST( 463 | LET user = DOCUMENT(users, @field_user.args.id) 464 | RETURN { 465 | _id: user._id, 466 | _key: user._key, 467 | _rev: user._rev, 468 | name: user.name, 469 | id: user._key, 470 | friends: ( 471 | FOR user_friends_node, user_friends IN ANY user friendOf 472 | RETURN { 473 | _id: user_friends._id, 474 | _key: user_friends._key, 475 | _rev: user_friends._rev, 476 | strength: user_friends.strength, 477 | user: FIRST( 478 | LET user_friends_user = user_friends_node 479 | RETURN { 480 | _id: user_friends_user._id, 481 | _key: user_friends_user._key, 482 | _rev: user_friends_user._rev, 483 | name: user_friends_user.name, 484 | id: user_friends_user._key 485 | } 486 | ) 487 | } 488 | ) 489 | } 490 | ) 491 | RETURN query" 492 | `); 493 | expect(mockRunQuery.mock.calls[0][0].bindVars).toMatchInlineSnapshot(` 494 | Object { 495 | "field_user": Object { 496 | "args": Object { 497 | "id": "foo", 498 | }, 499 | }, 500 | } 501 | `); 502 | }); 503 | 504 | test('runs arbitrary subqueries', async () => { 505 | await run( 506 | ` 507 | query GetUserAndFriends { 508 | user(id: "foo") { 509 | id 510 | name 511 | 512 | friendsOfFriends { 513 | id 514 | name 515 | } 516 | } 517 | } 518 | `, 519 | [ 520 | { 521 | id: 'foo', 522 | name: 'Bar', 523 | friendsOfFriends: [ 524 | { 525 | id: 'baz', 526 | name: 'Eva', 527 | }, 528 | ], 529 | }, 530 | ] 531 | ); 532 | 533 | expect(mockRunQuery.mock.calls[0][0].query).toMatchInlineSnapshot(` 534 | "LET query = FIRST( 535 | LET user = DOCUMENT(users, @field_user.args.id) 536 | RETURN { 537 | _id: user._id, 538 | _key: user._key, 539 | _rev: user._rev, 540 | name: user.name, 541 | id: user._key, 542 | friendsOfFriends: ( 543 | FOR user_friendsOfFriends IN 2..2 ANY user friendOf OPTIONS {bfs: true, uniqueVertices: 'path'} 544 | RETURN { 545 | _id: user_friendsOfFriends._id, 546 | _key: user_friendsOfFriends._key, 547 | _rev: user_friendsOfFriends._rev, 548 | name: user_friendsOfFriends.name, 549 | id: user_friendsOfFriends._key 550 | } 551 | ) 552 | } 553 | ) 554 | RETURN query" 555 | `); 556 | expect(mockRunQuery.mock.calls[0][0].bindVars).toMatchInlineSnapshot(` 557 | Object { 558 | "field_user": Object { 559 | "args": Object { 560 | "id": "foo", 561 | }, 562 | }, 563 | } 564 | `); 565 | }); 566 | 567 | test('uses context values', async () => { 568 | await run( 569 | ` 570 | query GetAuthorizedPosts { 571 | authorizedPosts { 572 | id 573 | title 574 | } 575 | } 576 | `, 577 | [ 578 | [ 579 | { 580 | id: 'a', 581 | title: 'Hello world', 582 | }, 583 | { 584 | id: 'b', 585 | title: 'Hello again world', 586 | }, 587 | ], 588 | ], 589 | { 590 | userId: 'foo', 591 | } 592 | ); 593 | 594 | expect(mockRunQuery.mock.calls[0][0].query).toMatchInlineSnapshot(` 595 | "LET query = ( 596 | LET authenticatedUser = DOCUMENT('users', @context.userId) 597 | LET allAuthorizedPosts = UNION_DISTINCT( 598 | (FOR post IN posts FILTER post.public == true RETURN post), 599 | (FOR post IN OUTBOUND authenticatedUser posted RETURN post) 600 | ) 601 | FOR authorizedPosts IN allAuthorizedPosts 602 | RETURN { 603 | _id: authorizedPosts._id, 604 | _key: authorizedPosts._key, 605 | _rev: authorizedPosts._rev, 606 | title: authorizedPosts.title, 607 | id: authorizedPosts._key 608 | } 609 | ) 610 | RETURN query" 611 | `); 612 | expect(mockRunQuery.mock.calls[0][0].bindVars).toMatchInlineSnapshot(` 613 | Object { 614 | "context": Object { 615 | "userId": "foo", 616 | }, 617 | } 618 | `); 619 | }); 620 | 621 | test('runs aql expressions', async () => { 622 | await run( 623 | ` 624 | query GetUserAndFriends { 625 | user(id: "foo") { 626 | id 627 | 628 | fullName 629 | } 630 | } 631 | `, 632 | [ 633 | { 634 | id: 'foo', 635 | fullName: 'Foo Bar', 636 | }, 637 | ] 638 | ); 639 | 640 | expect(mockRunQuery.mock.calls[0][0].query).toMatchInlineSnapshot(` 641 | "LET query = FIRST( 642 | LET user = DOCUMENT(users, @field_user.args.id) 643 | RETURN { 644 | _id: user._id, 645 | _key: user._key, 646 | _rev: user._rev, 647 | id: user._key, 648 | fullName: CONCAT(user.name, \\" \\", user.surname) 649 | } 650 | ) 651 | RETURN query" 652 | `); 653 | expect(mockRunQuery.mock.calls[0][0].bindVars).toMatchInlineSnapshot(` 654 | Object { 655 | "field_user": Object { 656 | "args": Object { 657 | "id": "foo", 658 | }, 659 | }, 660 | } 661 | `); 662 | }); 663 | 664 | test('does a Relay-style connection', async () => { 665 | await run( 666 | ` 667 | query GetUser { 668 | user(id: "foo") { 669 | id 670 | 671 | postsConnection(after: "opaqueCursor", filter: { publishedAfter: "2019-17-08 04:27:54 AM" }) { 672 | edges { 673 | cursor 674 | node { 675 | id 676 | title 677 | } 678 | } 679 | pageInfo { 680 | hasNextPage 681 | } 682 | } 683 | } 684 | } 685 | `, 686 | [ 687 | { 688 | id: 'foo', 689 | 690 | postsConnection: { 691 | edges: [ 692 | { 693 | cursor: 'a', 694 | node: { 695 | id: 'a', 696 | title: 'Hello world', 697 | }, 698 | }, 699 | ], 700 | pageInfo: { 701 | hasNextPage: false, 702 | }, 703 | }, 704 | }, 705 | ] 706 | ); 707 | 708 | expect(mockRunQuery.mock.calls[0][0].query).toMatchInlineSnapshot(` 709 | "LET query = FIRST( 710 | LET user = DOCUMENT(users, @field_user.args.id) 711 | RETURN { 712 | _id: user._id, 713 | _key: user._key, 714 | _rev: user._rev, 715 | id: user._key, 716 | postsConnection: FIRST( 717 | LET user_postsConnection_listPlusOne = ( 718 | FOR user_postsConnection_node, user_postsConnection_edge IN OUTBOUND user posted 719 | OPTIONS {bfs: true} 720 | FILTER user_postsConnection_node && (!@field_user_postsConnection.args.after || user_postsConnection_node.title > @field_user_postsConnection.args.after) && ( 721 | @field_user_postsConnection.args['filter'] != null && ( 722 | @field_user_postsConnection.args['filter'].publishedAfter == null || user_postsConnection_node.publishedAt > @field_user_postsConnection.args['filter'].publishedAfter 723 | ) && ( 724 | @field_user_postsConnection.args['filter'].titleLike == null || LIKE(user_postsConnection_node.title, CONCAT(\\"%\\", @field_user_postsConnection.args['filter'].titleLike, \\"%\\")) 725 | ) 726 | ) 727 | SORT user_postsConnection_node.title ASC 728 | LIMIT @field_user_postsConnection.args.first + 1 729 | RETURN MERGE(user_postsConnection_edge, { cursor: user_postsConnection_node.title, node: user_postsConnection_node }) 730 | ) 731 | LET user_postsConnection_pruned_edges = SLICE(user_postsConnection_listPlusOne, 0, @field_user_postsConnection.args.first) 732 | LET user_postsConnection = { 733 | edges: user_postsConnection_pruned_edges, 734 | pageInfo: { 735 | hasNextPage: LENGTH(user_postsConnection_listPlusOne) == @field_user_postsConnection.args.first + 1, 736 | startCursor: LENGTH(user_postsConnection_pruned_edges) > 0 ? FIRST(user_postsConnection_pruned_edges).cursor : null, 737 | endCursor: LENGTH(user_postsConnection_pruned_edges) > 0 ? LAST(user_postsConnection_pruned_edges).cursor : null 738 | } 739 | } 740 | RETURN { 741 | _id: user_postsConnection._id, 742 | _key: user_postsConnection._key, 743 | _rev: user_postsConnection._rev, 744 | edges: ( 745 | FOR user_postsConnection_edges IN user_postsConnection.edges 746 | RETURN { 747 | _id: user_postsConnection_edges._id, 748 | _key: user_postsConnection_edges._key, 749 | _rev: user_postsConnection_edges._rev, 750 | cursor: user_postsConnection_edges.cursor, 751 | node: FIRST( 752 | LET user_postsConnection_edges_node = user_postsConnection_edges.node 753 | RETURN { 754 | _id: user_postsConnection_edges_node._id, 755 | _key: user_postsConnection_edges_node._key, 756 | _rev: user_postsConnection_edges_node._rev, 757 | title: user_postsConnection_edges_node.title, 758 | id: user_postsConnection_edges_node._key 759 | } 760 | ) 761 | } 762 | ), 763 | pageInfo: FIRST( 764 | LET user_postsConnection_pageInfo = user_postsConnection.pageInfo 765 | RETURN { 766 | _id: user_postsConnection_pageInfo._id, 767 | _key: user_postsConnection_pageInfo._key, 768 | _rev: user_postsConnection_pageInfo._rev, 769 | hasNextPage: user_postsConnection_pageInfo.hasNextPage 770 | } 771 | ) 772 | } 773 | ) 774 | } 775 | ) 776 | RETURN query" 777 | `); 778 | expect(mockRunQuery.mock.calls[0][0].bindVars).toMatchInlineSnapshot(` 779 | Object { 780 | "field_user": Object { 781 | "args": Object { 782 | "id": "foo", 783 | }, 784 | }, 785 | "field_user_postsConnection": Object { 786 | "args": Object { 787 | "after": "opaqueCursor", 788 | "filter": Object { 789 | "publishedAfter": "2019-17-08 04:27:54 AM", 790 | }, 791 | "first": 10, 792 | }, 793 | }, 794 | } 795 | `); 796 | }); 797 | 798 | test('passes traversal options', async () => { 799 | await run( 800 | ` 801 | query GetUserAndPosts { 802 | user(id: "foo") { 803 | id 804 | name 805 | 806 | bfsPosts { 807 | id 808 | title 809 | } 810 | } 811 | } 812 | `, 813 | [ 814 | { 815 | id: 'foo', 816 | name: 'Foo', 817 | bfsPosts: [ 818 | { 819 | id: 'a', 820 | title: 'Hello world', 821 | }, 822 | { 823 | id: 'b', 824 | title: 'Hello again world', 825 | }, 826 | { 827 | id: 'c', 828 | title: 'Come here often, world?', 829 | }, 830 | ], 831 | }, 832 | ] 833 | ); 834 | 835 | expect(mockRunQuery.mock.calls[0][0].query).toMatchInlineSnapshot(` 836 | "LET query = FIRST( 837 | LET user = DOCUMENT(users, @field_user.args.id) 838 | RETURN { 839 | _id: user._id, 840 | _key: user._key, 841 | _rev: user._rev, 842 | name: user.name, 843 | id: user._key, 844 | bfsPosts: ( 845 | FOR user_bfsPosts IN OUTBOUND user posted 846 | OPTIONS { bfs: true } 847 | RETURN { 848 | _id: user_bfsPosts._id, 849 | _key: user_bfsPosts._key, 850 | _rev: user_bfsPosts._rev, 851 | title: user_bfsPosts.title, 852 | id: user_bfsPosts._key 853 | } 854 | ) 855 | } 856 | ) 857 | RETURN query" 858 | `); 859 | expect(mockRunQuery.mock.calls[0][0].bindVars).toMatchInlineSnapshot(` 860 | Object { 861 | "field_user": Object { 862 | "args": Object { 863 | "id": "foo", 864 | }, 865 | }, 866 | } 867 | `); 868 | }); 869 | 870 | test('resolves a custom query inside the resolver with selections', async () => { 871 | await run( 872 | ` 873 | mutation CreateUser { 874 | createUser { 875 | id 876 | name 877 | 878 | simplePosts { 879 | id 880 | title 881 | } 882 | } 883 | } 884 | `, 885 | [ 886 | { 887 | id: 'foobar', 888 | name: 'Bob', 889 | simplePosts: [], 890 | }, 891 | ] 892 | ); 893 | 894 | expect(mockRunQuery.mock.calls[0][0].query).toMatchInlineSnapshot(` 895 | "LET query = FIRST( 896 | LET createUser = FIRST( 897 | 898 | INSERT {_key: @value0, role: @value1, name: @value2} INTO users 899 | RETURN NEW 900 | 901 | ) 902 | RETURN { 903 | _id: createUser._id, 904 | _key: createUser._key, 905 | _rev: createUser._rev, 906 | name: createUser.name, 907 | id: createUser._key, 908 | simplePosts: ( 909 | FOR createUser_simplePosts IN OUTBOUND createUser posted 910 | RETURN { 911 | _id: createUser_simplePosts._id, 912 | _key: createUser_simplePosts._key, 913 | _rev: createUser_simplePosts._rev, 914 | title: createUser_simplePosts.title, 915 | id: createUser_simplePosts._key 916 | } 917 | ) 918 | } 919 | ) 920 | RETURN query" 921 | `); 922 | expect(mockRunQuery.mock.calls[0][0].bindVars).toMatchInlineSnapshot(` 923 | Object { 924 | "value0": "foobar", 925 | "value1": "captain", 926 | "value2": "Bob", 927 | } 928 | `); 929 | }); 930 | 931 | test('resolves a custom builder-based query with conditional behavior', async () => { 932 | await run( 933 | ` 934 | query SearchPosts { 935 | posts(searchTerm: "foo") { 936 | edges { 937 | node { 938 | id 939 | title 940 | } 941 | } 942 | } 943 | } 944 | `, 945 | [ 946 | { 947 | edges: [ 948 | { 949 | node: { 950 | id: 'a', 951 | title: 'foo', 952 | }, 953 | }, 954 | { 955 | node: { 956 | id: 'b', 957 | title: 'foobar', 958 | }, 959 | }, 960 | ], 961 | }, 962 | ] 963 | ); 964 | 965 | expect(mockRunQuery.mock.calls[0][0].query).toMatchInlineSnapshot(` 966 | "LET query = FIRST( 967 | LET posts_listPlusOne = ( 968 | FOR posts_node IN SearchView SEARCH PHRASE(posts_node.name, @field_posts.args.searchTerm, 'text_en') 969 | FILTER (!@field_posts.args.after || BM25(posts_node) > @field_posts.args.after) && true 970 | SORT BM25(posts_node) DESC 971 | LIMIT @field_posts.args.first + 1 972 | RETURN { cursor: BM25(posts_node), node: posts_node } 973 | ) 974 | LET posts_pruned_edges = SLICE(posts_listPlusOne, 0, @field_posts.args.first) 975 | LET posts = { 976 | edges: posts_pruned_edges, 977 | pageInfo: { 978 | hasNextPage: LENGTH(posts_listPlusOne) == @field_posts.args.first + 1, 979 | startCursor: LENGTH(posts_pruned_edges) > 0 ? FIRST(posts_pruned_edges).cursor : null, 980 | endCursor: LENGTH(posts_pruned_edges) > 0 ? LAST(posts_pruned_edges).cursor : null 981 | } 982 | } 983 | RETURN { 984 | _id: posts._id, 985 | _key: posts._key, 986 | _rev: posts._rev, 987 | edges: ( 988 | FOR posts_edges IN posts.edges 989 | RETURN { 990 | _id: posts_edges._id, 991 | _key: posts_edges._key, 992 | _rev: posts_edges._rev, 993 | node: FIRST( 994 | LET posts_edges_node = posts_edges.node 995 | RETURN { 996 | _id: posts_edges_node._id, 997 | _key: posts_edges_node._key, 998 | _rev: posts_edges_node._rev, 999 | title: posts_edges_node.title, 1000 | id: posts_edges_node._key 1001 | } 1002 | ) 1003 | } 1004 | ) 1005 | } 1006 | ) 1007 | RETURN query" 1008 | `); 1009 | expect(mockRunQuery.mock.calls[0][0].bindVars).toMatchInlineSnapshot(` 1010 | Object { 1011 | "field_posts": Object { 1012 | "args": Object { 1013 | "first": 10, 1014 | "searchTerm": "foo", 1015 | }, 1016 | }, 1017 | } 1018 | `); 1019 | 1020 | await run( 1021 | ` 1022 | query SearchPosts { 1023 | posts { 1024 | edges { 1025 | node { 1026 | id 1027 | title 1028 | } 1029 | } 1030 | } 1031 | } 1032 | `, 1033 | [ 1034 | { 1035 | edges: [ 1036 | { 1037 | node: { 1038 | id: 'c', 1039 | title: 'baz', 1040 | }, 1041 | }, 1042 | { 1043 | node: { 1044 | id: 'd', 1045 | title: 'bop', 1046 | }, 1047 | }, 1048 | ], 1049 | }, 1050 | ] 1051 | ); 1052 | 1053 | expect(mockRunQuery.mock.calls[1][0].query).toMatchInlineSnapshot(` 1054 | "LET query = FIRST( 1055 | LET posts_listPlusOne = ( 1056 | FOR posts_node IN posts 1057 | FILTER (!@field_posts.args.after || posts_node.createdAt > @field_posts.args.after) && true 1058 | SORT posts_node.createdAt ASC 1059 | LIMIT @field_posts.args.first + 1 1060 | RETURN { cursor: posts_node.createdAt, node: posts_node } 1061 | ) 1062 | LET posts_pruned_edges = SLICE(posts_listPlusOne, 0, @field_posts.args.first) 1063 | LET posts = { 1064 | edges: posts_pruned_edges, 1065 | pageInfo: { 1066 | hasNextPage: LENGTH(posts_listPlusOne) == @field_posts.args.first + 1, 1067 | startCursor: LENGTH(posts_pruned_edges) > 0 ? FIRST(posts_pruned_edges).cursor : null, 1068 | endCursor: LENGTH(posts_pruned_edges) > 0 ? LAST(posts_pruned_edges).cursor : null 1069 | } 1070 | } 1071 | RETURN { 1072 | _id: posts._id, 1073 | _key: posts._key, 1074 | _rev: posts._rev, 1075 | edges: ( 1076 | FOR posts_edges IN posts.edges 1077 | RETURN { 1078 | _id: posts_edges._id, 1079 | _key: posts_edges._key, 1080 | _rev: posts_edges._rev, 1081 | node: FIRST( 1082 | LET posts_edges_node = posts_edges.node 1083 | RETURN { 1084 | _id: posts_edges_node._id, 1085 | _key: posts_edges_node._key, 1086 | _rev: posts_edges_node._rev, 1087 | title: posts_edges_node.title, 1088 | id: posts_edges_node._key 1089 | } 1090 | ) 1091 | } 1092 | ) 1093 | } 1094 | ) 1095 | RETURN query" 1096 | `); 1097 | expect(mockRunQuery.mock.calls[1][0].bindVars).toMatchInlineSnapshot(` 1098 | Object { 1099 | "field_posts": Object { 1100 | "args": Object { 1101 | "first": 10, 1102 | }, 1103 | }, 1104 | } 1105 | `); 1106 | }); 1107 | 1108 | test('resolves multi-query operations to avoid read-after-write errors', async () => { 1109 | const result = await run( 1110 | ` 1111 | mutation CreatePost { 1112 | createPost { 1113 | post { 1114 | id 1115 | title 1116 | author { 1117 | id 1118 | } 1119 | } 1120 | } 1121 | } 1122 | `, 1123 | [ 1124 | { 1125 | postId: '3', 1126 | }, 1127 | { 1128 | id: '3', 1129 | title: 'Fake post', 1130 | author: { 1131 | id: 'foo', 1132 | }, 1133 | }, 1134 | ] 1135 | ); 1136 | 1137 | expect(result).toEqual({ 1138 | createPost: { 1139 | post: { 1140 | id: '3', 1141 | title: 'Fake post', 1142 | author: { 1143 | id: 'foo', 1144 | }, 1145 | }, 1146 | }, 1147 | }); 1148 | 1149 | expect(mockRunQuery).toHaveBeenCalledTimes(2); 1150 | expect(mockRunQuery.mock.calls[0][0].query).toMatchInlineSnapshot(` 1151 | "LET query = FIRST( 1152 | INSERT { title: \\"Fake post\\", body: \\"foo\\", publishedAt: \\"2019-05-03\\" } 1153 | INTO posts 1154 | OPTIONS { waitForSync: true } 1155 | LET createPost = { 1156 | post: NEW 1157 | } 1158 | RETURN { 1159 | _id: createPost._id, 1160 | _key: createPost._key, 1161 | _rev: createPost._rev, 1162 | post: createPost.post 1163 | } 1164 | ) 1165 | RETURN query" 1166 | `); 1167 | expect(mockRunQuery.mock.calls[0][0].bindVars).toMatchInlineSnapshot( 1168 | `Object {}` 1169 | ); 1170 | expect(mockRunQuery.mock.calls[1][0].query).toMatchInlineSnapshot(` 1171 | "LET query = FIRST( 1172 | LET post = DOCUMENT(posts, @parent._key) 1173 | RETURN { 1174 | _id: post._id, 1175 | _key: post._key, 1176 | _rev: post._rev, 1177 | title: post.title, 1178 | id: post._key, 1179 | author: FIRST( 1180 | FOR post_author IN INBOUND post posted 1181 | LIMIT 1 1182 | RETURN { 1183 | _id: post_author._id, 1184 | _key: post_author._key, 1185 | _rev: post_author._rev, 1186 | id: post_author._key 1187 | } 1188 | ) 1189 | } 1190 | ) 1191 | RETURN query" 1192 | `); 1193 | expect(mockRunQuery.mock.calls[1][0].bindVars).toMatchInlineSnapshot(` 1194 | Object { 1195 | "parent": Object { 1196 | "postId": "3", 1197 | }, 1198 | } 1199 | `); 1200 | }); 1201 | }); 1202 | -------------------------------------------------------------------------------- /src/buildQuery.ts: -------------------------------------------------------------------------------- 1 | import { DBQuery } from './types'; 2 | import { createAllReplacer } from './utils/plugins'; 3 | import { lines, indent } from './utils/strings'; 4 | 5 | type QueryBuilderArgs = { 6 | query: DBQuery; 7 | fieldName: string; 8 | parentName: string; 9 | }; 10 | 11 | export const buildQuery = ({ 12 | query, 13 | fieldName, 14 | }: Omit): string => { 15 | return lines([ 16 | `LET query = ${buildSubQuery({ query, fieldName, parentName: '@parent' })}`, 17 | `RETURN query`, 18 | ]); 19 | }; 20 | 21 | export const buildSubQuery = ({ 22 | query, 23 | fieldName, 24 | parentName, 25 | }: QueryBuilderArgs): string => { 26 | const { directiveArgs, builder } = query.builder; 27 | const fieldArgs = query.params.args || {}; 28 | const interpolate = createAllReplacer({ 29 | fieldName, 30 | parentName, 31 | }); 32 | 33 | const children = () => buildReturnProjection({ query, fieldName }); 34 | 35 | return interpolate( 36 | builder.build({ 37 | fieldName, 38 | parentName, 39 | fieldArgs, 40 | directiveArgs, 41 | returnsList: query.returnsList, 42 | children, 43 | }) 44 | ); 45 | }; 46 | 47 | const buildReturnProjection = ({ 48 | query, 49 | fieldName, 50 | }: Omit): string => { 51 | if (!query.fieldNames.length) { 52 | return `RETURN ${fieldName}`; 53 | } 54 | 55 | const scalarFields = query.fieldNames.filter( 56 | name => !query.fieldQueries[name] 57 | ); 58 | const nonScalarFields = query.fieldNames.filter( 59 | name => query.fieldQueries[name] 60 | ); 61 | 62 | return lines([ 63 | `RETURN {`, 64 | lines( 65 | [ 66 | // always include meta information. this allows disconnected queries to use $parent more 67 | // seamlessly by referencing $parent to traverse relationships, etc 68 | lines( 69 | [ 70 | `_id: ${fieldName}._id`, 71 | `_key: ${fieldName}._key`, 72 | `_rev: ${fieldName}._rev`, 73 | ].map(indent), 74 | ',\n' 75 | ), 76 | lines( 77 | scalarFields.map(name => `${name}: ${fieldName}.${name}`).map(indent), 78 | ',\n' 79 | ), 80 | lines( 81 | nonScalarFields 82 | .map(name => { 83 | const fieldQuery = query.fieldQueries[name]; 84 | const subQueryString = buildSubQuery({ 85 | query: fieldQuery, 86 | fieldName: joinFieldNames(fieldName, name), 87 | parentName: fieldName, 88 | }); 89 | return `${name}: ${subQueryString}`; 90 | }) 91 | .map(indent), 92 | ',\n' 93 | ), 94 | ], 95 | ',\n' 96 | ), 97 | `}`, 98 | ]); 99 | }; 100 | 101 | const joinFieldNames = (baseName: string, name: string) => 102 | `${baseName}_${name}`; 103 | -------------------------------------------------------------------------------- /src/builderInstanceCreators.ts: -------------------------------------------------------------------------------- 1 | import { BuilderInstance } from './types'; 2 | import builders from './builders'; 3 | 4 | export const builderCreators = { 5 | aql: (args: { expression: string }): BuilderInstance => ({ 6 | builder: builders.aql, 7 | directiveArgs: args, 8 | }), 9 | 10 | aqlDocument: (args: { 11 | collection: string; 12 | key: string; 13 | filter?: string; 14 | sort?: AqlSortInput; 15 | limit: AqlLimitInput; 16 | }): BuilderInstance => ({ 17 | builder: builders.aqlDocument, 18 | directiveArgs: args, 19 | }), 20 | 21 | aqlEdge: (args: { 22 | direction: AqlEdgeDirection; 23 | collection: string; 24 | options?: AqlTraverseOptionsInput; 25 | }): BuilderInstance => ({ 26 | builder: builders.aqlEdge, 27 | directiveArgs: args, 28 | }), 29 | 30 | aqlNode: (args: { 31 | edgeCollection: string; 32 | direction: AqlEdgeDirection; 33 | filter?: string; 34 | sort?: AqlSortInput; 35 | limit?: AqlLimitInput; 36 | options?: AqlTraverseOptionsInput; 37 | }): BuilderInstance => ({ 38 | builder: builders.aqlNode, 39 | directiveArgs: args, 40 | }), 41 | 42 | aqlEdgeNode: (): BuilderInstance => ({ 43 | builder: builders.aqlEdgeNode, 44 | directiveArgs: {}, 45 | }), 46 | 47 | aqlSubquery: (args: { query: string; return?: string }): BuilderInstance => ({ 48 | builder: builders.aqlSubquery, 49 | directiveArgs: args, 50 | }), 51 | 52 | aqlKey: (): BuilderInstance => ({ 53 | builder: builders.aqlKey, 54 | directiveArgs: {}, 55 | }), 56 | 57 | aqlRelayConnection: (args: { 58 | edgeCollection?: string; 59 | edgeDirection?: AqlEdgeDirection; 60 | documentCollection?: string; 61 | cursorExpression?: string; 62 | source?: string; 63 | filter?: string; 64 | sortOrder?: AqlSortOrder; 65 | }): BuilderInstance => ({ 66 | builder: builders.aqlRelayConnection, 67 | directiveArgs: args, 68 | }), 69 | 70 | aqlRelayEdges: (): BuilderInstance => ({ 71 | builder: builders.aqlRelayEdges, 72 | directiveArgs: {}, 73 | }), 74 | 75 | aqlRelayPageInfo: (): BuilderInstance => ({ 76 | builder: builders.aqlRelayPageInfo, 77 | directiveArgs: {}, 78 | }), 79 | 80 | aqlRelayNode: (): BuilderInstance => ({ 81 | builder: builders.aqlRelayNode, 82 | directiveArgs: {}, 83 | }), 84 | }; 85 | 86 | export type AqlEdgeDirection = 'INBOUND' | 'OUTBOUND'; 87 | export type AqlSortOrder = 'ASC' | 'DESC'; 88 | 89 | export type AqlSortInput = { 90 | property: string; 91 | order?: AqlSortOrder; 92 | sortOn?: string; 93 | }; 94 | 95 | export type AqlLimitInput = { 96 | count: string; 97 | skip?: string; 98 | }; 99 | 100 | export type AqlTraverseOptionsInput = { 101 | bfs?: boolean; 102 | uniqueVertices?: string; 103 | uniqueEdges?: string; 104 | }; 105 | -------------------------------------------------------------------------------- /src/builders/aql.ts: -------------------------------------------------------------------------------- 1 | import { Builder } from '../types'; 2 | 3 | export const aql: Builder = { 4 | name: 'aql', 5 | build: ({ directiveArgs }) => { 6 | return directiveArgs.expression; 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /src/builders/aqlDocument.ts: -------------------------------------------------------------------------------- 1 | import { Builder } from '../types'; 2 | import { lines, indent } from '../utils/strings'; 3 | import { buildQueryModifiers, buildSubquery } from '../utils/aql'; 4 | 5 | export const aqlDocument: Builder = { 6 | name: 'aqlDocument', 7 | build: ({ directiveArgs, returnsList, children }) => { 8 | const { collection, key } = directiveArgs; 9 | 10 | if (returnsList) { 11 | return buildSubquery( 12 | lines([ 13 | `FOR $field IN ${collection}`, 14 | indent(buildQueryModifiers(directiveArgs)), 15 | children(), 16 | ]), 17 | returnsList 18 | ); 19 | } 20 | 21 | // for a singular field without a key arg, we just take the first 22 | // item out of the list 23 | if (!returnsList && !key) { 24 | return buildSubquery( 25 | lines([ 26 | `LET $field = FIRST(`, 27 | indent(`FOR $field_i IN ${collection}`), 28 | indent( 29 | buildQueryModifiers({ 30 | ...directiveArgs, 31 | limit: { 32 | count: '1', 33 | }, 34 | }) 35 | ), 36 | indent(`RETURN $field_i`), 37 | `)`, 38 | children(), 39 | ]), 40 | returnsList 41 | ); 42 | } 43 | 44 | // possibly dangerous? a check to see if this is meant to be an interpolation 45 | // or if we need to treat it as a literal string 46 | const resolvedKey = key.startsWith('$') ? key : `"${key}"`; 47 | return buildSubquery( 48 | lines([ 49 | `LET $field = DOCUMENT(${collection}, ${resolvedKey})`, 50 | children(), 51 | ]), 52 | returnsList 53 | ); 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /src/builders/aqlEdge.ts: -------------------------------------------------------------------------------- 1 | import { Builder } from '../types'; 2 | import { lines, indent } from '../utils/strings'; 3 | import { buildQueryModifiers, buildSubquery, buildOptions } from '../utils/aql'; 4 | 5 | export const aqlEdge: Builder = { 6 | name: 'aqlEdge', 7 | build: ({ directiveArgs, returnsList, children }) => { 8 | const { direction, collection, options } = directiveArgs; 9 | 10 | return buildSubquery( 11 | lines([ 12 | `FOR $field_node, $field IN ${direction} $parent ${collection}`, 13 | indent(buildOptions(options)), 14 | indent( 15 | buildQueryModifiers({ 16 | ...directiveArgs, 17 | // enforce count 1 if this only resolves a single value 18 | limit: returnsList ? directiveArgs.limit : { count: '1' }, 19 | }) 20 | ), 21 | children(), 22 | ]), 23 | returnsList 24 | ); 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/builders/aqlEdgeNode.ts: -------------------------------------------------------------------------------- 1 | import { Builder } from '../types'; 2 | import { lines } from '../utils/strings'; 3 | import { buildSubquery } from '../utils/aql'; 4 | 5 | export const aqlEdgeNode: Builder = { 6 | name: 'aqlEdgeNode', 7 | build: ({ parentName, returnsList, children }) => { 8 | // this is assuming we are in the scope of a parent @edge subquery 9 | return buildSubquery( 10 | lines([`LET $field = ${parentName}_node`, children()]), 11 | returnsList 12 | ); 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/builders/aqlId.ts: -------------------------------------------------------------------------------- 1 | import { Builder } from '../types'; 2 | 3 | export const aqlId: Builder = { 4 | name: 'aqlId', 5 | build: () => `$parent._id`, 6 | }; 7 | -------------------------------------------------------------------------------- /src/builders/aqlKey.ts: -------------------------------------------------------------------------------- 1 | import { Builder } from '../types'; 2 | 3 | export const aqlKey: Builder = { 4 | name: 'aqlKey', 5 | build: () => `$parent._key`, 6 | }; 7 | -------------------------------------------------------------------------------- /src/builders/aqlNode.ts: -------------------------------------------------------------------------------- 1 | import { Builder } from '../types'; 2 | import { lines, indent } from '../utils/strings'; 3 | import { buildQueryModifiers, buildSubquery, buildOptions } from '../utils/aql'; 4 | 5 | export const aqlNode: Builder = { 6 | name: 'aqlNode', 7 | build: ({ directiveArgs, returnsList, children }) => { 8 | const { direction, edgeCollection, options } = directiveArgs; 9 | 10 | return buildSubquery( 11 | lines([ 12 | `FOR $field IN ${direction} $parent ${edgeCollection}`, 13 | indent(buildOptions(options)), 14 | indent( 15 | buildQueryModifiers({ 16 | ...directiveArgs, 17 | limit: returnsList ? directiveArgs.limit : { count: '1' }, 18 | }) 19 | ), 20 | children(), 21 | ]), 22 | returnsList 23 | ); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/builders/aqlRelayConnection.ts: -------------------------------------------------------------------------------- 1 | import { Builder } from '../types'; 2 | import { lines, indent } from '../utils/strings'; 3 | import { buildSubquery } from '../utils/aql'; 4 | 5 | export const aqlRelayConnection: Builder = { 6 | name: 'aqlRelayConnection', 7 | build: ({ directiveArgs, returnsList, children }) => { 8 | const { edgeCollection, source } = directiveArgs; 9 | 10 | if (!source && !edgeCollection) { 11 | throw new Error( 12 | 'Either edgeCollection or a custom source must be supplied to a Relay collection directive' 13 | ); 14 | } 15 | 16 | const listPlusOneSubquery = createListPlusOneSubquery(directiveArgs); 17 | 18 | return buildSubquery( 19 | lines([ 20 | `LET $field_listPlusOne = ${listPlusOneSubquery}`, 21 | `LET $field_pruned_edges = SLICE($field_listPlusOne, 0, $args.first)`, 22 | `LET $field = {`, 23 | indent(`edges: $field_pruned_edges,`), 24 | indent(`pageInfo: { `), 25 | indent( 26 | lines( 27 | [ 28 | indent( 29 | `hasNextPage: LENGTH($field_listPlusOne) == $args.first + 1` 30 | ), 31 | indent( 32 | `startCursor: LENGTH($field_pruned_edges) > 0 ? FIRST($field_pruned_edges).cursor : null` 33 | ), 34 | indent( 35 | `endCursor: LENGTH($field_pruned_edges) > 0 ? LAST($field_pruned_edges).cursor : null` 36 | ), 37 | ], 38 | ',\n' 39 | ) 40 | ), 41 | indent('}'), 42 | `}`, 43 | children(), 44 | ]), 45 | returnsList 46 | ); 47 | }, 48 | }; 49 | 50 | const createListPlusOneSubquery = (directiveArgs: any) => { 51 | const { 52 | cursorExpression: userCursorExpression, 53 | edgeDirection, 54 | edgeCollection, 55 | source, 56 | filter, 57 | sortOrder = 'ASC', 58 | } = directiveArgs; 59 | 60 | const cursorExpression = 61 | (userCursorExpression && 62 | interpolateUserCursorExpression(userCursorExpression)) || 63 | '$field_node._key'; 64 | 65 | const userFilter = filter ? interpolateUserCursorExpression(filter) : 'true'; 66 | 67 | const cursorFilter = `(!$args.after || ${cursorExpression} > $args.after)`; 68 | 69 | if (source) { 70 | return buildSubquery( 71 | lines([ 72 | interpolateUserCursorExpression(source), 73 | `FILTER ${cursorFilter} && ${userFilter}`, 74 | `SORT ${cursorExpression} ${sortOrder}`, 75 | `LIMIT $args.first + 1`, 76 | `RETURN { cursor: ${cursorExpression}, node: $field_node }`, 77 | ]), 78 | true 79 | ); 80 | } 81 | 82 | return buildSubquery( 83 | lines([ 84 | `FOR $field_node, $field_edge IN ${edgeDirection} $parent ${edgeCollection}`, 85 | indent(`OPTIONS {bfs: true}`), 86 | // filter out 'detached' edges which don't point to a node anymore 87 | `FILTER $field_node && ${cursorFilter} && ${userFilter}`, 88 | `SORT ${cursorExpression} ${sortOrder}`, 89 | `LIMIT $args.first + 1`, 90 | `RETURN MERGE($field_edge, { cursor: ${cursorExpression}, node: $field_node })`, 91 | ]), 92 | true 93 | ); 94 | }; 95 | 96 | // converts user-land "$node" and "$edge" into field-qualified interpolations used in the rest 97 | // of this plugin 98 | const interpolateUserCursorExpression = (cursorExpression: string) => 99 | cursorExpression 100 | .replace(/\$node/g, `$field_node`) 101 | .replace(/\$edge/g, `$field_edge`) 102 | .replace(/\$path/g, `$field_path`); 103 | -------------------------------------------------------------------------------- /src/builders/aqlRelayEdges.ts: -------------------------------------------------------------------------------- 1 | import { Builder } from '../types'; 2 | import { lines } from '../utils/strings'; 3 | import { buildSubquery } from '../utils/aql'; 4 | 5 | export const aqlRelayEdges: Builder = { 6 | name: 'aqlRelayEdges', 7 | build: ({ returnsList, children }) => 8 | buildSubquery( 9 | lines([`FOR $field IN $parent.edges`, children()]), 10 | returnsList 11 | ), 12 | }; 13 | -------------------------------------------------------------------------------- /src/builders/aqlRelayNode.ts: -------------------------------------------------------------------------------- 1 | import { Builder } from '../types'; 2 | import { lines } from '../utils/strings'; 3 | import { buildSubquery } from '../utils/aql'; 4 | 5 | export const aqlRelayNode: Builder = { 6 | name: 'aqlRelayNode', 7 | build: ({ returnsList, children }) => 8 | buildSubquery( 9 | lines([`LET $field = $parent.node`, children()]), 10 | returnsList 11 | ), 12 | }; 13 | -------------------------------------------------------------------------------- /src/builders/aqlRelayPageInfo.ts: -------------------------------------------------------------------------------- 1 | import { Builder } from '../types'; 2 | import { lines } from '../utils/strings'; 3 | import { buildSubquery } from '../utils/aql'; 4 | 5 | export const aqlRelayPageInfo: Builder = { 6 | name: 'aqlRelayPageInfo', 7 | build: ({ returnsList, children }) => 8 | buildSubquery( 9 | lines([`LET $field = $parent.pageInfo`, children()]), 10 | returnsList 11 | ), 12 | }; 13 | -------------------------------------------------------------------------------- /src/builders/aqlSubquery.ts: -------------------------------------------------------------------------------- 1 | import { Builder } from '../types'; 2 | import { lines } from '../utils/strings'; 3 | import { buildSubquery } from '../utils/aql'; 4 | 5 | export const aqlSubquery: Builder = { 6 | name: 'aqlSubquery', 7 | build: ({ directiveArgs, returnsList, children }) => { 8 | const { query, return: ret } = directiveArgs; 9 | 10 | return buildSubquery( 11 | lines([ 12 | query, 13 | // if the user uses the "return" helper arg, we construct the right 14 | // field binding assignment for them to be returned by the return 15 | // projection construction 16 | ret && (returnsList ? `FOR $field IN ${ret}` : `LET $field = ${ret}`), 17 | children(), 18 | ]), 19 | returnsList 20 | ); 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /src/builders/index.ts: -------------------------------------------------------------------------------- 1 | import { aqlDocument } from './aqlDocument'; 2 | import { aqlNode } from './aqlNode'; 3 | import { aqlEdge } from './aqlEdge'; 4 | import { aqlEdgeNode } from './aqlEdgeNode'; 5 | import { aql } from './aql'; 6 | import { aqlSubquery } from './aqlSubquery'; 7 | import { aqlKey } from './aqlKey'; 8 | import { aqlRelayConnection } from './aqlRelayConnection'; 9 | import { aqlRelayEdges } from './aqlRelayEdges'; 10 | import { aqlRelayPageInfo } from './aqlRelayPageInfo'; 11 | import { aqlRelayNode } from './aqlRelayNode'; 12 | import { aqlId } from './aqlId'; 13 | 14 | export default { 15 | aqlDocument, 16 | aqlNode, 17 | aqlEdge, 18 | aqlEdgeNode, 19 | aql, 20 | aqlSubquery, 21 | aqlKey, 22 | aqlRelayConnection, 23 | aqlRelayEdges, 24 | aqlRelayPageInfo, 25 | aqlRelayNode, 26 | aqlId, 27 | }; 28 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const IGNORED_FIELD_NAMES = ['__typename']; 2 | export const FIELD_PARAM_PREFIX = 'field_'; 3 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | export class FieldMissingError extends Error { 2 | constructor(typeName: string, fieldName: string) { 3 | super( 4 | `Invalid state: field "${fieldName}" does not exist on type "${typeName}"` 5 | ); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/executeQuery.ts: -------------------------------------------------------------------------------- 1 | import { Database } from 'arangojs'; 2 | 3 | export const executeQuery = async ({ 4 | query, 5 | bindVars, 6 | db, 7 | fieldName, 8 | }: { 9 | query: string; 10 | bindVars: { [name: string]: any }; 11 | db: Database; 12 | fieldName: string; 13 | }) => { 14 | const queryResult = await db.query({ 15 | query, 16 | bindVars, 17 | }); 18 | 19 | const allResults = await queryResult.all(); 20 | return allResults[0]; 21 | }; 22 | -------------------------------------------------------------------------------- /src/extractQueries.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLResolveInfo, 3 | GraphQLObjectType, 4 | FieldNode, 5 | SelectionSetNode, 6 | } from 'graphql'; 7 | import { DBQuery, BuilderInstance, Builder, DBQueryParams } from './types'; 8 | import { IGNORED_FIELD_NAMES } from './constants'; 9 | import { getFieldDef } from 'graphql/execution/execute'; 10 | import { getFieldDirectives, getDirectiveArgs } from './utils/directives'; 11 | import { 12 | getArgumentsPlusDefaults, 13 | isListOrWrappedListType, 14 | extractObjectType, 15 | getNameOrAlias, 16 | } from './utils/graphql'; 17 | import { pathOr } from 'ramda'; 18 | import { getFieldPath } from './utils/graphql'; 19 | import defaultPlugins from './builders'; 20 | 21 | type CommonExtractionParams = { 22 | info: GraphQLResolveInfo; 23 | parentQuery: DBQuery | undefined; 24 | parentType: GraphQLObjectType; 25 | path: string[]; 26 | builders: { [directiveName: string]: Builder }; 27 | argumentResolvers: { [path: string]: any }; 28 | }; 29 | 30 | export const extractQueriesFromResolveInfo = ({ 31 | info, 32 | builders = defaultPlugins, 33 | argumentResolvers = {}, 34 | }: { 35 | info: GraphQLResolveInfo; 36 | builders?: { [directiveName: string]: Builder }; 37 | argumentResolvers?: { [path: string]: any }; 38 | }) => 39 | extractQueriesFromField({ 40 | info, 41 | parentQuery: undefined, 42 | parentType: info.parentType, 43 | field: info.fieldNodes[0], 44 | path: getFieldPath(info), 45 | builders: builders, 46 | argumentResolvers, 47 | }); 48 | 49 | export const extractQueriesFromField = ({ 50 | info, 51 | parentQuery, 52 | parentType, 53 | field, 54 | path, 55 | builders: builders, 56 | argumentResolvers, 57 | }: CommonExtractionParams & { 58 | field: FieldNode; 59 | }): DBQuery | null => { 60 | const fieldName = field.name.value; 61 | 62 | if (IGNORED_FIELD_NAMES.includes(fieldName)) { 63 | return null; 64 | } 65 | 66 | if (parentQuery) { 67 | parentQuery.fieldNames.push(fieldName); 68 | } 69 | 70 | const schemaFieldDef = getFieldDef(info.schema, parentType, fieldName); 71 | if (!schemaFieldDef) { 72 | throw new Error( 73 | `Invalid state: there's no field definition for field "${fieldName}" on type "${parentType.name}"` 74 | ); 75 | } 76 | 77 | const directives = getFieldDirectives(parentType, fieldName); 78 | 79 | // abort this path if there is an @aqlNewQuery directive and this is not the root field 80 | if ( 81 | parentQuery && 82 | directives.some(({ name }) => name.value === 'aqlNewQuery') 83 | ) { 84 | return null; 85 | } 86 | 87 | const builderDirective = directives.find( 88 | directive => !!builders[directive.name.value] 89 | ); 90 | 91 | if (!builderDirective) { 92 | return null; 93 | } 94 | 95 | const builderInstance = { 96 | builder: builders[builderDirective.name.value], 97 | directiveArgs: getDirectiveArgs(builderDirective, info.variableValues), 98 | } as BuilderInstance; 99 | 100 | if (!builderInstance) { 101 | return null; 102 | } 103 | 104 | const argValues = getArgumentsPlusDefaults( 105 | parentType.name, 106 | field, 107 | info.schema, 108 | info.variableValues 109 | ); 110 | 111 | const paramNames: string[] = []; 112 | const params: DBQueryParams = {}; 113 | 114 | if (Object.keys(argValues).length) { 115 | paramNames.push('args'); 116 | // process via arg resolver if it exists 117 | const argResolver = pathOr((a: any) => a, path, argumentResolvers); 118 | const resolvedArgs = argResolver(argValues); 119 | params.args = resolvedArgs; 120 | } 121 | 122 | const baseQuery = { 123 | returnsList: isListOrWrappedListType(schemaFieldDef.type), 124 | builder: builderInstance, 125 | paramNames, 126 | params, 127 | fieldNames: [], 128 | fieldQueries: {}, 129 | }; 130 | 131 | if (!field.selectionSet) { 132 | return baseQuery; 133 | } 134 | 135 | const currentTypeAsObjectType = extractObjectType(schemaFieldDef.type); 136 | 137 | if (!currentTypeAsObjectType) { 138 | return baseQuery; 139 | } 140 | 141 | baseQuery.fieldQueries = extractQueriesFromSelectionSet({ 142 | selectionSet: field.selectionSet, 143 | parentQuery: baseQuery, 144 | parentType: currentTypeAsObjectType, 145 | info, 146 | path, 147 | builders: builders, 148 | argumentResolvers, 149 | }); 150 | 151 | return baseQuery; 152 | }; 153 | 154 | export const extractQueriesFromSelectionSet = ({ 155 | selectionSet, 156 | path, 157 | ...rest 158 | }: CommonExtractionParams & { 159 | selectionSet: SelectionSetNode; 160 | }): { [field: string]: DBQuery | null } => 161 | selectionSet.selections.reduce((reducedQueries, selection) => { 162 | if (selection.kind === 'Field') { 163 | return { 164 | ...reducedQueries, 165 | [getNameOrAlias(selection)]: extractQueriesFromField({ 166 | field: selection, 167 | path: [...path, getNameOrAlias(selection)], 168 | ...rest, 169 | }), 170 | }; 171 | } else if (selection.kind === 'InlineFragment') { 172 | return { 173 | ...reducedQueries, 174 | ...extractQueriesFromSelectionSet({ 175 | selectionSet: selection.selectionSet, 176 | path, 177 | ...rest, 178 | }), 179 | }; 180 | } else { 181 | const fragment = rest.info.fragments[selection.name.value]; 182 | return { 183 | ...reducedQueries, 184 | ...extractQueriesFromSelectionSet({ 185 | selectionSet: fragment.selectionSet, 186 | path, 187 | ...rest, 188 | }), 189 | }; 190 | } 191 | }, {}); 192 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { resolver } from './resolver'; 2 | 3 | export * from './runCustomQuery'; 4 | export { createResolver } from './resolver'; 5 | export * from './typeDefs'; 6 | export { resolver }; 7 | export { builderCreators as builders } from './builderInstanceCreators'; 8 | 9 | export default resolver; 10 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | const tag = '[graphql-arangodb] '; 4 | 5 | export const log = ({ 6 | title, 7 | level, 8 | details, 9 | }: { 10 | title: string; 11 | level: 'info' | 'debug' | 'verbose' | 'error'; 12 | details: string[]; 13 | }) => { 14 | if (level === 'verbose' && __DEV__ && process.env.DEBUG) { 15 | console.debug( 16 | [chalk.yellow(tag + title), ...details.map(str => chalk.gray(str))].join( 17 | '\n' 18 | ) 19 | ); 20 | } else if (level === 'debug' && process.env.DEBUG) { 21 | console.debug( 22 | [chalk.cyan(tag + title), ...details.map(str => chalk.gray(str))].join( 23 | '\n' 24 | ) 25 | ); 26 | } else if (level === 'error') { 27 | console.error([tag + title, ...details].join('\n')); 28 | } else if (level === 'info') { 29 | console.info( 30 | [chalk.blue(tag + title), ...details.map(str => chalk.gray(str))].join( 31 | '\n' 32 | ) 33 | ); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/resolver.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLResolveInfo } from 'graphql'; 2 | import { LibraryOptions, AqlResolver } from './types'; 3 | import defaultBuilders from './builders'; 4 | import { extractQueriesFromResolveInfo } from './extractQueries'; 5 | import { runQuery } from './runQuery'; 6 | import { createCustomQueryRunner } from './runCustomQuery'; 7 | 8 | export const createResolver = (options: LibraryOptions) => { 9 | const aqlResolver = async ( 10 | parent: any, 11 | args: { [key: string]: any }, 12 | context: any, 13 | info: GraphQLResolveInfo 14 | ) => { 15 | const { builders = defaultBuilders, argumentResolvers = {} } = options; 16 | 17 | const query = extractQueriesFromResolveInfo({ 18 | info, 19 | builders, 20 | argumentResolvers, 21 | }); 22 | 23 | if (!query) { 24 | return null; 25 | } 26 | 27 | return runQuery({ 28 | options, 29 | info, 30 | query, 31 | context, 32 | parent, 33 | }); 34 | }; 35 | 36 | /** 37 | * Construct your own AQL query within a resolver, using whatever logic you wish, 38 | * then pass it to this function (along with bindVars). Also pass in the parent, 39 | * context and info arguments of your resolver. This function will take your original 40 | * query and append additional AQL to it so that it will resolve the rest of the 41 | * GraphQL query selections the user has made. 42 | */ 43 | (aqlResolver as any).runCustomQuery = createCustomQueryRunner(options); 44 | 45 | return aqlResolver as AqlResolver; 46 | }; 47 | 48 | export const resolver = createResolver({}); 49 | -------------------------------------------------------------------------------- /src/runCustomQuery.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLResolveInfo } from 'graphql'; 2 | import { LibraryOptions, Builder, BuilderInstance } from './types'; 3 | import defaultBuilders from './builders'; 4 | import { 5 | isListOrWrappedListType, 6 | extractObjectType, 7 | getFieldPath, 8 | } from './utils/graphql'; 9 | import { extractQueriesFromSelectionSet } from './extractQueries'; 10 | import { runQuery } from './runQuery'; 11 | import { lines } from './utils/strings'; 12 | import { buildSubquery } from './utils/aql'; 13 | import { AqlQuery } from 'arangojs/lib/cjs/aql-query'; 14 | 15 | export const createCustomQueryRunner = (options: LibraryOptions) => async ({ 16 | info, 17 | context, 18 | parent, 19 | query, 20 | queryBuilder: providedBuilder, 21 | args, 22 | }: { 23 | query?: AqlQuery; 24 | queryBuilder?: BuilderInstance; 25 | info: GraphQLResolveInfo; 26 | context: any; 27 | parent: any; 28 | args: { [name: string]: any }; 29 | }) => { 30 | const { builders = defaultBuilders, argumentResolvers = {} } = options; 31 | 32 | if (!providedBuilder && !query) { 33 | throw new Error('At least one of queryBuilder or query must be provided'); 34 | } 35 | 36 | const customQueryBuilder: Builder = { 37 | name: 'customQuery', 38 | build: ({ children, returnsList }) => 39 | buildSubquery( 40 | lines([ 41 | `LET $field = ${buildSubquery( 42 | (query as AqlQuery).query, 43 | returnsList 44 | )}`, 45 | children(), 46 | ]), 47 | returnsList 48 | ), 49 | }; 50 | 51 | const builderQuery = { 52 | returnsList: isListOrWrappedListType(info.returnType), 53 | builder: providedBuilder || { 54 | builder: customQueryBuilder, 55 | directiveArgs: {}, 56 | }, 57 | 58 | paramNames: ['args'], 59 | params: { 60 | args, 61 | }, 62 | fieldNames: [], 63 | fieldQueries: {}, 64 | }; 65 | 66 | const selectionSet = info.fieldNodes[0].selectionSet; 67 | const returnTypeAsObjectType = extractObjectType(info.returnType); 68 | 69 | if (!selectionSet || !returnTypeAsObjectType) { 70 | throw new Error( 71 | 'Not implemented: custom query without GraphQL selection or Object return type' 72 | ); 73 | } 74 | 75 | builderQuery.fieldQueries = extractQueriesFromSelectionSet({ 76 | selectionSet, 77 | info, 78 | path: getFieldPath(info), 79 | parentQuery: builderQuery, 80 | parentType: returnTypeAsObjectType, 81 | builders, 82 | argumentResolvers, 83 | }); 84 | 85 | return runQuery({ 86 | options, 87 | context, 88 | query: builderQuery, 89 | info, 90 | parent, 91 | additionalBindVars: query && query.bindVars, 92 | }); 93 | }; 94 | 95 | export const runCustomQuery = createCustomQueryRunner({}); 96 | -------------------------------------------------------------------------------- /src/runQuery.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLResolveInfo } from 'graphql'; 2 | import { DBQuery, LibraryOptions } from './types'; 3 | import { buildQuery } from './buildQuery'; 4 | import { buildPrefixedVariables } from './utils/variables'; 5 | import { executeQuery } from './executeQuery'; 6 | import { log } from './logger'; 7 | 8 | export const runQuery = async ({ 9 | options, 10 | context, 11 | query, 12 | info, 13 | additionalBindVars, 14 | parent, 15 | }: { 16 | options: LibraryOptions; 17 | context: any; 18 | query: DBQuery; 19 | info: GraphQLResolveInfo; 20 | additionalBindVars?: { [name: string]: any }; 21 | parent: any; 22 | }) => { 23 | const { 24 | db, 25 | contextKey = 'arangoContext', 26 | contextDbKey = 'arangoDb', 27 | } = options; 28 | 29 | const resolvedDb = db || context[contextDbKey]; 30 | if (!resolvedDb) { 31 | throw new Error( 32 | `Either a valid ArangoDB Database instance must be supplied on the "${contextDbKey}" property of the context, or you must create your own resolver using createResolver from graphql-arangodb` 33 | ); 34 | } 35 | 36 | try { 37 | const queryString = buildQuery({ 38 | query, 39 | fieldName: info.fieldName, 40 | }); 41 | 42 | const bindVars = buildPrefixedVariables({ 43 | fieldName: info.fieldName, 44 | query, 45 | parent, 46 | contextValues: context[contextKey], 47 | queryString, 48 | }); 49 | 50 | log({ 51 | title: `Running query`, 52 | level: 'info', 53 | details: [queryString, JSON.stringify(bindVars)], 54 | }); 55 | 56 | const data = await executeQuery({ 57 | query: queryString, 58 | bindVars: { 59 | ...(additionalBindVars || {}), 60 | ...bindVars, 61 | }, 62 | db: resolvedDb, 63 | fieldName: info.fieldName, 64 | }); 65 | 66 | log({ 67 | title: `Query response data`, 68 | level: 'debug', 69 | details: [JSON.stringify(data)], 70 | }); 71 | 72 | return data; 73 | } catch (err) { 74 | log({ 75 | title: `Query execution error`, 76 | level: 'error', 77 | details: [err.toString(), (err as Error).stack], 78 | }); 79 | throw err; 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /src/typeDefs.ts: -------------------------------------------------------------------------------- 1 | export const directiveTypeDefs = ` 2 | """Represents the direction of an edge in the graph relative to the current node""" 3 | enum AqlEdgeDirection { 4 | OUTBOUND 5 | INBOUND 6 | ANY 7 | } 8 | 9 | """Represents the order of a sorting operation""" 10 | enum AqlSortOrder { 11 | DESC 12 | ASC 13 | } 14 | 15 | input AqlSortInput { 16 | """The property to sort on""" 17 | property: String! 18 | """The order to sort in. Defaults ASC""" 19 | order: AqlSortOrder = ASC 20 | """Change the object being sorted. Defaults to $field""" 21 | sortOn: String 22 | } 23 | 24 | input AqlLimitInput { 25 | """The upper limit of documents to return""" 26 | count: String! 27 | """The number of documents to skip""" 28 | skip: String 29 | } 30 | 31 | input AqlTraverseOptionsInput { 32 | """Enables breadth-first search""" 33 | bfs: Boolean 34 | """ 35 | - "path": guarantees no path is returned with a duplicate vertex 36 | - "global": guarantees each vertex is visited at most once for the whole traversal 37 | - "none": (default) no uniqueness check 38 | """ 39 | uniqueVertices: String 40 | """ 41 | - "path": (default) guarantees no path is returned with a duplicate edge 42 | - "none": allows paths to 'double back' onto edges cyclically 43 | """ 44 | uniqueEdges: String 45 | } 46 | 47 | directive @aqlDocument( 48 | collection: String! 49 | key: String 50 | filter: String 51 | sort: AqlSortInput 52 | limit: AqlLimitInput 53 | ) on FIELD_DEFINITION 54 | 55 | directive @aqlNode( 56 | edgeCollection: String! 57 | direction: AqlEdgeDirection! 58 | filter: String 59 | sort: AqlSortInput 60 | limit: AqlLimitInput 61 | options: AqlTraverseOptionsInput 62 | ) on FIELD_DEFINITION 63 | 64 | directive @aqlEdge( 65 | direction: AqlEdgeDirection! 66 | collection: String! 67 | filter: String 68 | sort: AqlSortInput 69 | limit: AqlLimitInput 70 | options: AqlTraverseOptionsInput 71 | ) on FIELD_DEFINITION 72 | 73 | directive @aqlEdgeNode on FIELD_DEFINITION 74 | 75 | directive @aql( 76 | expression: String! 77 | ) on FIELD_DEFINITION 78 | 79 | directive @aqlSubquery( 80 | query: String! 81 | return: String 82 | ) on FIELD_DEFINITION 83 | 84 | directive @aqlKey on FIELD_DEFINITION 85 | 86 | directive @aqlRelayConnection( 87 | edgeCollection: String 88 | edgeDirection: AqlEdgeDirection 89 | cursorExpression: String 90 | source: String 91 | filter: String 92 | sortOrder: AqlSortOrder = ASC 93 | ) on FIELD_DEFINITION | OBJECT 94 | 95 | directive @aqlRelayEdges on FIELD_DEFINITION | OBJECT 96 | 97 | directive @aqlRelayPageInfo on FIELD_DEFINITION | OBJECT 98 | 99 | directive @aqlRelayNode on FIELD_DEFINITION | OBJECT 100 | 101 | directive @aqlNewQuery on FIELD_DEFINITION 102 | `; 103 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { Database } from 'arangojs'; 2 | import { GraphQLResolveInfo } from 'graphql'; 3 | import { AqlQuery } from 'arangojs/lib/cjs/aql-query'; 4 | export type DBQuery = { 5 | returnsList: boolean; 6 | 7 | fieldNames: string[]; 8 | fieldQueries: { 9 | [name: string]: DBQuery; 10 | }; 11 | 12 | paramNames: string[]; 13 | params: DBQueryParams; 14 | 15 | builder: BuilderInstance; 16 | }; 17 | 18 | export type DBQueryParams = { 19 | args?: { [name: string]: any }; 20 | }; 21 | 22 | export type QueryFieldMap = { 23 | [path: string]: DBQuery; 24 | }; 25 | 26 | export type Builder = { 27 | name: string; 28 | build: (args: { 29 | fieldName: string; 30 | directiveArgs: { [name: string]: any }; 31 | fieldArgs: { [name: string]: any }; 32 | returnsList: boolean; 33 | parentName: string; 34 | children: () => string; 35 | }) => string; 36 | }; 37 | 38 | export type BuilderInstance = { 39 | builder: Builder; 40 | directiveArgs: { [name: string]: any }; 41 | }; 42 | 43 | export type LibraryOptions = { 44 | builders?: { [name: string]: Builder }; 45 | argumentResolvers?: { [pathPart: string]: any }; 46 | contextKey?: string; 47 | db?: Database; 48 | contextDbKey?: string; 49 | }; 50 | 51 | export type AqlResolver = { 52 | (parent: any, args: any, context: any, info: GraphQLResolveInfo): Promise< 53 | any 54 | >; 55 | runCustomQuery: (args: { 56 | query?: AqlQuery; 57 | queryBuilder?: BuilderInstance; 58 | info: GraphQLResolveInfo; 59 | parent: any; 60 | args: any; 61 | context: any; 62 | }) => Promise; 63 | }; 64 | -------------------------------------------------------------------------------- /src/utils/__tests__/plugins.test.ts: -------------------------------------------------------------------------------- 1 | import { createFieldArgGetter, createArgReplacer } from '../plugins'; 2 | 3 | test('arg replacer', () => { 4 | const getter = createFieldArgGetter('field_name'); 5 | const argReplacer = createArgReplacer(getter); 6 | 7 | expect(argReplacer('$args')).toEqual('@field_field_name.args'); 8 | expect(argReplacer('$args.foo.bar')).toEqual( 9 | '@field_field_name.args.foo.bar' 10 | ); 11 | expect(argReplacer("$args['foo'].bar")).toEqual( 12 | "@field_field_name.args['foo'].bar" 13 | ); 14 | expect(argReplacer('$args["foo"].bar')).toEqual( 15 | '@field_field_name.args["foo"].bar' 16 | ); 17 | expect(argReplacer('$args[$args.foo].bar')).toEqual( 18 | '@field_field_name.args[@field_field_name.args.foo].bar' 19 | ); 20 | }); 21 | -------------------------------------------------------------------------------- /src/utils/aql.ts: -------------------------------------------------------------------------------- 1 | import { lines, indent } from './strings'; 2 | /** 3 | * Statement builders for AQL 4 | */ 5 | 6 | export const buildLimit = (count: string, skip?: string) => { 7 | if (skip) { 8 | return `LIMIT ${skip} ${count}`; 9 | } 10 | 11 | return `LIMIT ${count}`; 12 | }; 13 | 14 | export const buildFilter = (condition: string) => `FILTER ${condition}`; 15 | 16 | export const buildSort = ( 17 | property: string, 18 | order: string = 'ASC', 19 | sortOn: string = '$field' 20 | ) => `SORT ${sortOn}[${interpolationOrString(property)}] ${order}`; 21 | 22 | export const buildQueryModifiers = ({ 23 | limit, 24 | filter, 25 | sort, 26 | }: { 27 | limit?: { 28 | count: string; 29 | skip?: string; 30 | }; 31 | filter?: string; 32 | sort?: { 33 | property: string; 34 | order: string; 35 | sortOn?: string; 36 | }; 37 | }): string => 38 | lines([ 39 | filter && buildFilter(filter), 40 | sort && buildSort(sort.property, sort.order, sort.sortOn), 41 | limit && buildLimit(limit.count, limit.skip), 42 | ]); 43 | 44 | export const buildSubquery = (contents: string, returnsList: boolean) => 45 | lines([`${returnsList ? '' : 'FIRST'}(`, indent(contents), `)`]); 46 | 47 | export const buildOptions = (options?: { 48 | bfs: boolean; 49 | uniqueVertices: string; 50 | uniqueEdges: string; 51 | }) => { 52 | if (!options) { 53 | return ''; 54 | } 55 | 56 | const pairs = [ 57 | options.bfs !== undefined ? `bfs: ${options.bfs}` : '', 58 | options.uniqueVertices !== undefined 59 | ? `uniqueVertices: "${options.uniqueVertices}"` 60 | : '', 61 | options.uniqueEdges !== undefined 62 | ? `uniqueEdges: "${options.uniqueEdges}"` 63 | : '', 64 | ] 65 | .filter(Boolean) 66 | .join(', '); 67 | 68 | return `OPTIONS { ${pairs} }`; 69 | }; 70 | 71 | const interpolationOrString = (value: string) => 72 | value.startsWith('$') ? value : JSON.stringify(value); 73 | -------------------------------------------------------------------------------- /src/utils/directives.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLType, isObjectType, DirectiveNode } from 'graphql'; 2 | import { valueNodeToValue } from './graphql'; 3 | 4 | export const hasDirective = ( 5 | parentType: GraphQLType, 6 | fieldName: string, 7 | directiveName: string 8 | ) => { 9 | const directives = getFieldDirectives(parentType, fieldName); 10 | 11 | return directives.some(({ name }) => name.value === directiveName); 12 | }; 13 | 14 | export const getFieldDirectiveArgs = ( 15 | parentType: GraphQLType, 16 | fieldName: string, 17 | directiveName: string, 18 | variableValues: { [name: string]: any } 19 | ) => { 20 | const directives = getFieldDirectives(parentType, fieldName); 21 | 22 | const directive = directives.find(({ name }) => name.value === directiveName); 23 | 24 | return getDirectiveArgs(directive, variableValues); 25 | }; 26 | 27 | export const getDirectiveArgs = ( 28 | directive: DirectiveNode | undefined, 29 | variableValues: { [name: string]: any } 30 | ) => { 31 | if (!directive || !directive.arguments) { 32 | return {}; 33 | } 34 | 35 | return directive.arguments.reduce( 36 | (args, arg) => ({ 37 | ...args, 38 | [arg.name.value]: valueNodeToValue(arg.value, variableValues), 39 | }), 40 | {} 41 | ); 42 | }; 43 | 44 | export const getFieldDirectives = ( 45 | parentType: GraphQLType, 46 | fieldName: string 47 | ) => { 48 | if (!isObjectType(parentType)) { 49 | throw new Error( 50 | `Cannot traverse field "${fieldName}" on non-object type "${parentType}"` 51 | ); 52 | } 53 | 54 | const field = parentType.getFields()[fieldName]; 55 | 56 | if (!field) { 57 | throw new Error( 58 | `Field "${fieldName}" does not exist on type "${parentType.name}"` 59 | ); 60 | } 61 | 62 | if (!field.astNode) { 63 | throw new Error( 64 | `Field "${parentType.name}.${field}" doesn't have an AST node` 65 | ); 66 | } 67 | 68 | return field.astNode.directives || []; 69 | }; 70 | -------------------------------------------------------------------------------- /src/utils/graphql.ts: -------------------------------------------------------------------------------- 1 | import { FieldMissingError } from '../errors'; 2 | import { 3 | GraphQLOutputType, 4 | isListType, 5 | isNonNullType, 6 | FieldNode, 7 | GraphQLObjectType, 8 | GraphQLSchema, 9 | isObjectType, 10 | ValueNode, 11 | NameNode, 12 | GraphQLResolveInfo, 13 | ResponsePath, 14 | } from 'graphql'; 15 | 16 | export const getArgumentsPlusDefaults = ( 17 | parentTypeName: string, 18 | field: FieldNode, 19 | schema: GraphQLSchema, 20 | variables: { [name: string]: any } 21 | ): { [name: string]: any } => { 22 | const schemaType = schema.getType(parentTypeName); 23 | 24 | if (!schemaType || !isObjectType(schemaType)) { 25 | throw new Error( 26 | `Invalid state: Unknown or non-object type name "${parentTypeName}" (type: ${schemaType})` 27 | ); 28 | } 29 | 30 | const schemaField = schemaType.getFields()[field.name.value]; 31 | 32 | if (!schemaField) { 33 | throw new FieldMissingError(schemaType.name, field.name.value); 34 | } 35 | 36 | const defaults = schemaField.args.reduce( 37 | (argMap, arg) => 38 | arg.defaultValue !== undefined 39 | ? { ...argMap, [arg.name]: arg.defaultValue } 40 | : argMap, 41 | {} 42 | ); 43 | 44 | return { 45 | ...defaults, 46 | ...argFieldsToValues({}, field.arguments || [], variables), 47 | }; 48 | }; 49 | 50 | export const argFieldsToValues = ( 51 | providedValues: { [key: string]: any }, 52 | fields: readonly { value: ValueNode; name: NameNode }[], 53 | variables: { [variableName: string]: any } 54 | ) => 55 | fields.reduce((acc, fieldNode) => { 56 | acc[fieldNode.name.value] = valueNodeToValue(fieldNode.value, variables); 57 | return acc; 58 | }, providedValues); 59 | 60 | export const valueNodeToValue = ( 61 | valueNode: ValueNode, 62 | variables: { [variableName: string]: any } 63 | ): any => { 64 | if (valueNode.kind === 'Variable') { 65 | return variables[valueNode.name.value]; 66 | } else if (valueNode.kind === 'NullValue') { 67 | return null; 68 | } else if (valueNode.kind === 'ObjectValue') { 69 | return argFieldsToValues({}, valueNode.fields, variables); 70 | } else if (valueNode.kind === 'ListValue') { 71 | return valueNode.values.map(value => valueNodeToValue(value, variables)); 72 | } else if (valueNode.kind === 'IntValue') { 73 | return parseInt(valueNode.value, 10); 74 | } else if (valueNode.kind === 'FloatValue') { 75 | return parseFloat(valueNode.value); 76 | } else { 77 | return valueNode.value; 78 | } 79 | }; 80 | 81 | export const isListOrWrappedListType = (type: GraphQLOutputType): boolean => { 82 | if (isListType(type)) { 83 | return true; 84 | } 85 | if (isNonNullType(type)) { 86 | return isListOrWrappedListType(type.ofType); 87 | } 88 | return false; 89 | }; 90 | 91 | export const getNameOrAlias = (field: FieldNode): string => 92 | field.alias ? field.alias.value : field.name.value; 93 | 94 | export const extractObjectType = ( 95 | type: GraphQLOutputType 96 | ): GraphQLObjectType | null => { 97 | if (isObjectType(type)) { 98 | return type; 99 | } 100 | 101 | // TODO: interface / union ? 102 | 103 | if (isNonNullType(type) || isListType(type)) { 104 | return extractObjectType(type.ofType); 105 | } 106 | 107 | return null; 108 | }; 109 | 110 | export const getIsRootField = (info: GraphQLResolveInfo): boolean => 111 | [info.schema.getQueryType(), info.schema.getMutationType()] 112 | .filter(Boolean) 113 | .some(rootType => !!rootType && rootType.name === info.parentType.name); 114 | 115 | /** 116 | * Converts a path from `info` into a field path, skipping over 117 | * array indices (since they are not represented in the schema 118 | * field selection paths) 119 | */ 120 | export function getFieldPath(info: GraphQLResolveInfo) { 121 | const path: string[] = []; 122 | let pathLink: ResponsePath | undefined = info.path; 123 | while (pathLink) { 124 | if (typeof pathLink.key === 'string') { 125 | path.unshift(pathLink.key); 126 | } 127 | pathLink = pathLink.prev; 128 | } 129 | 130 | return path; 131 | } 132 | -------------------------------------------------------------------------------- /src/utils/plugins.ts: -------------------------------------------------------------------------------- 1 | import { FIELD_PARAM_PREFIX } from '../constants'; 2 | 3 | export const createFieldArgGetter = (fieldName: string) => ( 4 | argPath: string 5 | ) => { 6 | return argPath.replace(/\$args/g, `@${FIELD_PARAM_PREFIX}${fieldName}.args`); 7 | }; 8 | 9 | /** 10 | * Creates a function which replaces all 11 | * "$args" 12 | * "$args.foo.bar" or 13 | * "$args['foo'].bar" or 14 | * "$args["foo"].bar" or 15 | * "$args[$args.foo].bar" 16 | * with the real argument string 17 | */ 18 | export const createArgReplacer = (argGetter: (name: string) => any) => ( 19 | str: string 20 | ) => { 21 | const argMatcher = /\$args([\.\[]\w[\w\d]+)*/; 22 | let result; 23 | let modifiedStr = '' + str; 24 | 25 | while ((result = argMatcher.exec(modifiedStr)) !== null) { 26 | const text = result[0]; 27 | const index = result.index; 28 | const splicedString = spliceString( 29 | modifiedStr, 30 | index, 31 | text, 32 | argGetter(text) 33 | ); 34 | if (splicedString === modifiedStr) { 35 | // sanity check to avoid infinite looping 36 | throw new Error( 37 | 'Infinite loop detected while interpolating query. This is probably a bug in graphql-arangodb. Please file a bug report with the GraphQL SDL + directives your query is evaluating!' 38 | ); 39 | } 40 | modifiedStr = splicedString; 41 | } 42 | 43 | return modifiedStr; 44 | }; 45 | 46 | /** 47 | * Creates a function which replaces all "$field" with the actual field name 48 | */ 49 | const createFieldReplacer = (fieldName: string) => (text: string) => 50 | replaceAll(text, '$field', fieldName); 51 | const createParentReplacer = (parentName: string) => (text: string) => 52 | replaceAll(text, '$parent', parentName); 53 | const createContextReplacer = () => (text: string) => 54 | replaceAll(text, '$context', '@context'); 55 | 56 | const replaceAll = ( 57 | text: string, 58 | original: string, 59 | replacement: string 60 | ): string => { 61 | let modifiedText = '' + text; 62 | let index; 63 | while ((index = modifiedText.indexOf(original)) >= 0) { 64 | modifiedText = spliceString(modifiedText, index, original, replacement); 65 | } 66 | return modifiedText; 67 | }; 68 | 69 | const spliceString = ( 70 | text: string, 71 | index: number, 72 | original: string, 73 | replacement: string 74 | ) => { 75 | return ( 76 | text.slice(0, index) + replacement + text.slice(index + original.length) 77 | ); 78 | }; 79 | 80 | export const createAllReplacer = ({ 81 | fieldName, 82 | parentName, 83 | }: { 84 | fieldName: string; 85 | parentName: string; 86 | }) => { 87 | const argReplacer = createArgReplacer(createFieldArgGetter(fieldName)); 88 | const fieldReplacer = createFieldReplacer(fieldName); 89 | const parentReplacer = createParentReplacer(parentName); 90 | const contextReplacer = createContextReplacer(); 91 | 92 | return (text: string): string => 93 | contextReplacer(parentReplacer(fieldReplacer(argReplacer(text)))); 94 | }; 95 | -------------------------------------------------------------------------------- /src/utils/strings.ts: -------------------------------------------------------------------------------- 1 | export const lines = (strings: (string | undefined)[], joiner = '\n') => { 2 | const existingLines = strings.filter(Boolean); 3 | if (existingLines.length) { 4 | return existingLines.join(joiner); 5 | } 6 | return ''; 7 | }; 8 | 9 | export const indent = (line: string) => 10 | line && 11 | line 12 | .split('\n') 13 | .map(l => ` ${l}`) 14 | .join('\n'); 15 | -------------------------------------------------------------------------------- /src/utils/variables.ts: -------------------------------------------------------------------------------- 1 | import { DBQuery } from '../types'; 2 | import { FIELD_PARAM_PREFIX } from '../constants'; 3 | 4 | /** 5 | * recursively flattens and builds a set of arg object variables for a query 6 | * and all its sub-queries 7 | */ 8 | const buildPrefixedFieldArgVariables = ({ 9 | fieldName, 10 | query, 11 | }: { 12 | fieldName: string; 13 | query: DBQuery; 14 | }): { [name: string]: any } => ({ 15 | [`${FIELD_PARAM_PREFIX}${fieldName}`]: { 16 | args: query.params.args, 17 | }, 18 | ...query.fieldNames 19 | .filter((childFieldName: string) => !!query.fieldQueries[childFieldName]) 20 | .reduce( 21 | (args: { [name: string]: any }, childFieldName: string) => ({ 22 | ...args, 23 | ...buildPrefixedFieldArgVariables({ 24 | fieldName: fieldName + '_' + childFieldName, 25 | query: query.fieldQueries[childFieldName], 26 | }), 27 | }), 28 | {} 29 | ), 30 | }); 31 | 32 | export const buildPrefixedVariables = ({ 33 | fieldName, 34 | query, 35 | parent, 36 | contextValues, 37 | queryString, 38 | }: { 39 | fieldName: string; 40 | query: DBQuery; 41 | parent?: any; 42 | contextValues?: any; 43 | queryString: string; 44 | }) => { 45 | return filterUnused( 46 | { 47 | // passing the parent as a variable lets us cross the graphql -> graphdb boundary 48 | // and give queries access to their parent objects from our GraphQL context 49 | parent, 50 | // the user may supply values in their context which they always want passed to queries 51 | context: contextValues, 52 | 53 | ...buildPrefixedFieldArgVariables({ fieldName, query }), 54 | }, 55 | queryString 56 | ); 57 | }; 58 | 59 | const filterUnused = (vars: { [name: string]: any }, queryString: string) => 60 | Object.keys(vars).reduce((filtered, key) => { 61 | if (!new RegExp(`@${key}\\W`).test(queryString)) { 62 | return filtered; 63 | } 64 | return { 65 | ...filtered, 66 | [key]: vars[key], 67 | }; 68 | }, {}); 69 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "types"], 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "rootDir": "./", 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "strictNullChecks": true, 14 | "strictFunctionTypes": true, 15 | "strictPropertyInitialization": true, 16 | "noImplicitThis": true, 17 | "alwaysStrict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": false, 20 | "noImplicitReturns": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "moduleResolution": "node", 23 | "jsx": "react", 24 | "esModuleInterop": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare var __DEV__: boolean; 2 | --------------------------------------------------------------------------------