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.
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!
type Query {
user(id: ID!): User @aqlDocument(collection: "users", key: "$args.id")
}
type User {
friends: [FriendOfEdge!]!
@aqlEdge(
collection: "friendOf"
direction: ANY
sort: { property: "name", sortOn: "$field_node" }
)
}
type FriendOfEdge {
strength: Int
user: User! @aqlEdgeNode
}
type Mutation {
createPost(input: PostCreateInput!): Post!
@aqlSubquery(
query: """
INSERT { title: $args.input.title, body: $args.input.body } INTO posts
"""
return: "NEW"
)
}
For a simple and small example, see the example directory.
For a larger scale app that uses this library, check out my now-defunct startup idea Toast. The toast-core microservice (linked) drove the entire development of this library and almost every feature is utilized in that codebase.
Start by installing the library
npm i --save graphql-arangodb
You may also need to install peer dependencies if you don't have them:
npm i --save graphql arangojs
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.
import { directiveTypeDefs } from 'graphql-arangodb';
const typeDefs = [directiveTypeDefs, ...allYourAppsOtherTypeDefs];
makeExecutableSchema({ typeDefs });
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:
const arangoDb = new Database({
url: 'http://localhost:8529',
});
arangoDb.useDatabase('mydb');
arangoDb.useBasicAuth('mysecretuser', 'mysecretpassword');
const context = {
arangoDb,
};
// pass the context into your GraphQL server according to documentation of the server
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.
For most people, adding the default aqlResolver from graphql-arangodb should be enough:
import aqlResolver from 'graphql-arangodb';
const resolvers = {
Query: {
user: aqlResolver,
users: aqlResolver,
// ...
},
};
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.
import { createResolver, plugins as defaultPlugins } from 'graphql-arangodb';
const resolver = createResolver({
// argument resolvers are called like regular resolvers, but they are used only by
// graphql-arangodb to apply custom transformations to field arguments before
// adding them to the AQL query. They are separated from normal resolvers for
// technical reasons related to how queries are extracted and built by the library.
// Whenver possible, prefer to put this logic inside the AQL query itself.
argumentResolvers: {
Query: {
searchUsers: args => ({
...args,
// apply Lucene fuzzy indicator to user's match string before passing it to AQL
match: `${args.match}~`,
}),
},
},
// customize the key in your context which stores data which will be passed down
// into AQL queries via the $context interpolation
contextKey: 'arango_context',
// customize the context property which is used to get your Database instance
contextDbKey: 'arango_db',
// advanced: you can reassign the names of the default directive plugins, or
// create your own plugin here. Plugins aren't documented yet, see source.
plugins: {
...defaultPlugin,
custom: myCustomPlugin,
},
// you can specify a static database instance instead of passing one through context
db: new Database(),
});
Now that the library is configured, you can start adding directives to indicate how to query for your data.
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.
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).
AqlEdgeDirection: OUTBOUND | INBOUND | ANYAqlSortOrder: DESC | ASCAqlRelayConnectionSource: Default | FullTextSome directives take complex inputs:
input AqlSortInput {
"""
The property to sort on
"""
property: String!
"""
The order to sort in. Defaults ASC
"""
order: AqlSortOrder = ASC
"""
Change the object being sorted. Defaults to $field
"""
sortOn: String
}
input AqlLimitInput {
"""
The upper limit of documents to return
"""
count: String!
"""
The number of documents to skip
"""
skip: String
}
"""
These are the same as the OPTIONS for a regular edge traversal in AQL
"""
input AqlTraversalOptionsInput {
bfs: Boolean
uniqueVertices: String
uniqueEdges: String
}
All directives support the following interpolations in their parameter values:
$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.$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).$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.$context: Reference values from the arangoContext key in your GraphQL context. Use this for global values across all queries, like the authenticated user ID.@aqlDocumentSelects 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.
Parameters
collection: String!: The name of the collection of documentskey: String: A string value or interpolation that indicates the database key of the document.filter: String: Adds a filter expression. Applies to key-based single document fetching (the first document will be taken after filter is applied).sort: AqlSortInput: Adds a sort expression. Applies to key-based single document fetching (the first document will be taken after sort is applied).limit: AqlLimitInput: Adds a limit expression. Only works when key is not provided.Example
type Query {
user(id: ID!): User @aqlDocument(collection: "users", key: "$args.id")
}
@aqlNodeTraverses 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.
Parameters
edgeCollection: String!: The name of the collection which the edge belongs todirection: AqlEdgeDirection!: The direction to traverse. Can be ANY.filter: String: Adds a filter expression.sort: AqlSortInput: Adds a sort expression.limit: AqlLimitInput: Adds a limit expression.options: AqlTraverseOptionsInput: Modify OPTIONS parameters on the traversal.Example
type User {
posts: [Post!]! @aqlNode(edgeCollection: "posted", direction: OUTBOUND)
}
@aqlEdge/@aqlEdgeNode@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.
Parameters
Only @aqlEdge takes parameters:
collection: String!: The name of the collection for the edgedirection: AqlEdgeDirection!: The direction to traverse. Can be ANY.filter: String: Adds a filter expression. To filter on the node, you can use $field_node as an interpolation. Defaults sortOn to $field.sort: AqlSortInput Adds a sort expression.limit: AqlLimitInput: Adds a limit expression.options: AqlTraverseOptionsInput: Modify OPTIONS parameters on the traversal.@aqlEdgeNode has no parameters.
Example
type User {
friends: [FriendOfEdge!]!
@aqlEdge(
collection: "friendOf"
direction: ANY
sort: { property: "name", sortOn: "$field_node" }
)
}
type FriendOfEdge {
strength: Int
user: User! @aqlEdgeNode
}
@aqlSubqueryConstruct a free-form subquery to resolve a field. There are important rules for your subquery:
$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.(). This is done by the library.RETURN statement. All RETURN projections are constructed by the library for you to match the GraphQL query.Parameters
query: String!: Your subquery string, following the rules listed above.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.Examples
Resolving a single value
type Query {
userCount: Int!
@aqlSubquery(
query: """
LET $field = LENGTH(users)
"""
)
}
Resolving multiple values
```graphql type Query { """ Merges the list of public posts with the list of posts the user has posted (even private) to create a master list of all posts accessible by the user. """ authorizedPosts: [Post!]! @aqlSubquery( query: """ LET authenticatedUser = DOCUMENT('users', $context.userId) LET allAuthorizedPosts = UNION_DISTINCT( (FOR post IN posts FILTER post.public == true RETURN post), (FOR post in OUTBOUND authenticatedUser posted RETURN post)
$ claude mcp add graphql-arangodb \
-- python -m otcore.mcp_server <graph>