Package: appsync-gremlin
AppSync - GremlinOverviewThrough the AppSync-Gremlin, developers can write powerful queries in GraphQL without having to worry
too much about the underlying database query language in AWS Neptune. The AppSync-Gremlin provides
lambda function code that converts query operation types (from GraphQL) to a gremlin traversal.DefinitionsProperty field: A field corresponding to a property of a vertex in the AWS Neptune graph database. In the below example,
thenamefield is a property field.{
  User {
      name
      following {
          name
      }
  }
}Query 1 : (Vertex Field Example)Vertex field: A field corresponding to a vertex in the AWS Neptune graph database. In the above example,locationis a vertex field.Vertex list fields: A field corresponding to a list of vertices in the AWS Neptune graph database. In the above example,followingis a vertex list field.Result set: An assignment of vertices in the graph to fields in the query. As the database processes the query, new
result sets may be created (e.g. when traversing edges), and result sets may be discarded when they do not satisfy filters.
After all parts of the query are processed by the database, all remaining result sets are used to form the query result, by taking their values at all
properties marked for output (anything in an output scope).Scope: The part of a query between any pair of parentheses or curly braces. We often refer to the parts between parentheses as theinput scopeand the parts between curly braces as theoutput scopeorpayload scope.
For example, consider the query{
  User (
      input: {
          name: {
              eq: "John"
          }
      }
  ) {
      name
      following {
          name
      }
  }
}Query 2 : (Scope Example)Filtering Operations and PaginationFilteringWe define a filtering standard on the following scalar fields:ID:
For ID filtering we define the following input for filtering:input IDFilterInput {
  ne: ID
  eq: ID

  in: [ID!]
  not_in: [ID!]
}String: For String filtering we define the following input for filteringinput StringFilterInput {
  ne: String
  eq: String

  in: [String!]
  not_in: [String!]

  contains: String
  not_contains: String

  begins_with: String
  not_begins_with: String

  ends_with: String
  not_ends_with: String
}Int: For Integer filtering we define the following input for filteringinput IntFilterInput {
  ne: Int
  eq: Int
  le: Int
  lt: Int
  ge: Int
  gt: Int

  in: [Int!]
  not_in: [int!]
}Float: For Float filtering we define the following input for filteringinput FloatFilterInput {
  ne: Float
  eq: Float
  le: Float
  lt: Float
  ge: Float
  gt: Float

  in: [Float!]
  not_in: [Float!]
}Boolean: For Boolean filtering we define the following input for filteringinput BooleanFilterInput {
  eq: Boolean
  ne: Boolean
}DateTime: For DateTime we first have to define our own DateTime input definition. To avoid confusion and to prevent the use of different DateTime formats
in this interface, we have defined the followingDateTimeInputto expose the individual date components (such as day, month, year, etc) as well
as aformattedfield which is the ISO 8601 string representation of the DateTime value:input DateTimeInput {
  year: Int
  month: Int
  day: Int
  hour: Int
  minute: Int
  second: Int
  formatted: DateTime #custom datetime scalar
}Using this input definition, we can then create the following input for filtering:input DateTimeFilterInput {
  eq: DateTimeInput
  ne: DateTimeInput

  in: [DateTimeInput!]
  not_in: [DateTimeInput!]

  le: DateTimeInput
  lt: DateTimeInput
  ge: DateTimeInput
  gt: DateTimeInput
}and a filtering standard on enum types:enum ENUM_FIELD_TYPE {
    E_1
    E_2
    .
    .
    .
    E_n
}

input EnumFilterInput {
    eq: ENUM_FIELD_TYPE
    ne: ENUM_FIELD_TYPE
    in: [ENUM_FIELD_TYPE!]
    not_in: [ENUM_FIELD_TYPE!]
}Note that these standards must be manually implemented in the original GraphQL schema. In future we may devise some method of augmenting a GraphQL schema
so we don't have to manually implement them.Each of these scalar filters has a corresponding filter in the AppSync-Gremlin library. For example, theStringFilterInputhas
the scalar filterstring_filter, etc.Implementing FiltersThe AppSync-Gremlin library also allows customvertex,relationshipandscalarfilters to be implemented.Scalar filters: We define ascalarfilter as a filter that maps GraphQL fields to Gremlin predicates. Consider anid_filter.
In GraphQL, theIDFilterInputhas the following fields:eq, ne, in, not_in. The Gremlin predicates that correspond to these fields areeq, neq, within, without. So we construct the following dictionary to map the GraphQL fields to the Gremlin predicates:fromgremlin_python.process.traversalimporteq,neq,within,withoutdefid_scalar_filter():return{"eq":eq,"ne":neq,"in":within,"not_in":without}By using this dictionary, we can construct a filter function that applies the predicates to some traversalg, such that the traverser is
at some verticesvwith propertyproperty_name,
producing a filtered traversal with the general form ,`g' = g.has(property_name, p_1).has(property_name, p_2). ... .has(property_name, p_n)`,wherep_1, p_2, ..., p_nare predicates that are applied to the propertyproperty_nameatv.The@scalar_filterdecorator provided by the AppSync-Gremlin library produces a function that takes aproperty_nameand returns the function described above. So by applying this to the aboveid_filtergives usfromgremlin_python.process.traversalimporteq,neq,within,withoutfromappsync_gremlinimportscalar_filter@scalar_filterdefid__filter():return{"eq":eq,"ne":neq,"in":within,"not_in":without}However, at this stage the filter cannot actually be used, since it requires aproperty_name. The AppSync-Gremlin library
provides two different ways of applying aproperty_nameto a filter. Either we can simply callid_scalar_filterwith out desiredproperty_name, or we can apply the@namedecorator.For scalar filters, we advice that the former is used. Since this provides a better generalisation for your scalar filter.
Since a vertex could implement it's own custom id with property name"id"or simply make use of the built in vertex idT.id.Vertex filter: We define avertexfilter as a filter that maps GraphQL fields toscalarorrelationship(see below) filters.
Let us consider the following GraphQLUsertype:type User {
    id: ID!
    email: String!

    name: String
    about: String

    following: [User]!
    followed_by: [User]!
}We have 4 scalar fields (id, email, name, about) and 2 vertex list fields (following, followed_by). In the Graph database, we define
aUservertex with propertiesemail, name, about(string) and a single relationshipUser -> FOLLOWS -> User. So in a similar fashion to
a scalar filter, let us define some function that returns a dictionary mapping the GraphQL fields to filters:fromgremlin_python.process.traversalimportTfromappsync_gremlinimportid_filter,string_filterdefuser_filter():return{"id":id_filter(T.id),"email":string_filter("email"),"name":string_filter("name"),"about":string_filter("about"),"following":?,"followed_by":?}By using this dictionary we can construct a filter function that applies thescalarandrelationshipfilters to some
supplied traversalg, such that the traverser is at some verticesvwhich matches the desired filter (in this case the travesers must all be atUservertices),
producing a filtered traversalg'with the general form:g' = r_1(r_2( ... (r_n(s_1( ... s_m(g) ... ))) ... ))wherer_1, r_2, ..., r_nare relationship filters ands_1, s_2, ..., s_mare scalar filters. Note that the order of application of scalar and relationship
filters does not matter, however, we advice that scalar filters should be applied first as it is more efficient.The@vertex_filterdecorator provided by the AppSync-Gremlin library produces a function that takes avertex_labeland returns the function described above. So by applying this to the aboveuser_filtergives usfromgremlin_python.process.traversalimportTfromappsync_gremlinimportid_filter,string_filter,vertex_filter,name@name("User")@vertex_filterdefuser_filter():return{"id":id_filter(T.id),"email":string_filter("email"),"name":string_filter("name"),"about":string_filter("about"),"following":?,"followed_by":?}Similarly to thescalar_filter, we can apply the@namedecorator to pass thevertex_labelto the filter function.
This label should match the label of theUservertex in the Graph database.With the vertex filter now completely implemented, we can now define our relationship filters (see below). In this case
we have the relationship filters:fromgremlin_python.process.traversalimportTfromappsync_gremlinimportid_filter,string_filter,vertex_filter,name,relationship_filter,RelationshipDirection@name("User")@vertex_filterdefuser_filter():return{"id":id_filter(T.id),"email":string_filter("email"),"name":string_filter("name"),"about":string_filter("about"),"following":relationship_filter(("FOLLOWS",RelationshipDirection.OUT),user_filter),"followed_by":relationship_filter(("FOLLOWS",RelationshipDirection.IN),user_filter)}Relationship filter: We define a relationshipRfrom some vertexutovasR = (name, direction). We can
filterubased on the whether a relationshipRexists and whethervsatisfies certain conditions.Consider a Gremlin traversalg, such that the traverser is at some verticesu. We can produce a filtered traversalg'based on whether the vertices selected byghave the relationshipRand the verticesv(the other vertex inR) satisfy
a vertex filterv_f. This filtered traversal has the general form:g' = g.where(v_f(direction(name))).The AppSync-Gremlin library produces a functionrelationship_filterthat takes arelationship, a tuple consisting of a
edge label (name) and a edge direction (direction), and a vertex filterv_ffor the other vertex in the relationship.(See above for example).PaginationWe also implement a pagination standard. Note that pagination can only be applied to vertex list fields.For simplicity, we've decided to implement an offset based pagination, as it allows us to make us
of the Gremlin traversal step.range(first, offset). The stanardised pagination input is defined as follows:input PaginationInput {
  page: Int!
  per_page: Int!
}We then usepageandper_pageto computefirstandoffsetusing the functionget_range, shown below.fromtypingimportTupledefget_range(page:int,per_page:int)->Tuple[int,int]:"""Returns the Gremlin range from page options in the format:(first, last):param page: (Integer):param per_page: (Integer):return: (Integer, Integer)"""return(page-1)*per_page,page*per_pageOnce the traversal has been submitted and the result set has been return, we format the response into a pagination
response object. The GraphQL type for this response object for some GraphQL typeTypeistype Type {
    .
    .
    .
}

type TypePage {
    data: [Type]!
    page: Int!
    per_page: Int!
    total: Int!
}wheretotalis thetotalnumber of pages available.Error Handling and Request / Response Mapping TemplateThe AppSync-Gremlin library provides automatic error handling for AppSync. The library does this via the user of theAppSyncException.
TheAppSyncExceptionrequires 3 arguments when instantiated:error_type,error_messageanderror_datafor type
string, string and dictionary respectively.For example, consider the mutation resolver that creates aUservertex. Naturally we want to ensure that a user doesn't have a duplicate vertex,
therefore we must add some form of validation within the resolver code which raises anAppSyncExceptionwith the relevant error information
if the validation fails.fromgremlin_python.process.graph_traversalimportGraphTraversalfromappsync_gremlinimportResolverInput,AppSyncException,mutation_resolver@mutation_resolverdefcreate_user(traversal:GraphTraversal,resolver_input:ResolverInput)->GraphTraversal:username=resolver_input.arguments.get("username")user=traversal.V().hasLabel("User").has("username",username)ifuser.hasNext():raiseAppSyncException(error_type="BAD_REQUEST",error_message="A user with username{}is already stored in the AWS Neptune database.".format(username),error_data={"username":username})...In order to ensure ourAppSyncExceptionwork's with AppSync, we've had to define a request / response template mapping standard.
For all resolvers, we must have the request template mapping:{
  "version" : "2018-05-29",
  "operation": "(Invoke|BatchInvoke)",
  "payload": {
    "type_name": String!,
    "field_name": String!,
    "arguments": $util.toJson($context.args),
    "identity": $util.toJson($context.identity),
    "source": $util.toJson($context.source)
  }
}and the response mapping template:#if ($context.result && $context.result.error)
    $utils.error($context.result.error.error_message, $context.result.error.error_type, $context.result.error.data)
#else
    $utils.toJson($context.result.data)
#endUsageThe AppSync-Gremlin library currently provides 4 different resolver types in the form of function decorators:vertex_list_field_resolververtex_field_resolvercalculated_field_resolvermutation_resolverThe first 3 resolvers are Query based resolvers, that is to say they are designed to be used for resolving GraphQL
queries.Each resolver has the same function type signature:resolver : (GraphTraversal, ResolverInput) -> GraphTraversalNote the customResolverInputobject. AResolverInputobject simply stores the data passed from the Apache VTL
request mapping template described in the section above. Hence theResolverInputobject has the following properties:type_name:(String)field_name:(String)arguments:(Dictionary)identity:(Dictionary | None)source:(Dictionary | None)Hence these properties can be referenced in the resolvers to build the Gremlin traversals.