├── .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 |
--------------------------------------------------------------------------------