Giter Club home page Giter Club logo

woo.sh's Introduction

woo.sh

woo.sh is a software framework that can automatically generate all necessary components to set up a full GraphQL server that creates and stores a graph database within a separate database management system (DBMS) and that provides a feature-rich GraphQL API to access this database. The only input that is needed for this process is the schema of the graph database that is to be created and used. Then, the main features of the generated GraphQL API are:

  • creation and updates of the nodes in the graph database (including the properties of these nodes);
  • creation and updates of the edges in the graph database (including the properties of these edges);
  • automatic enforcement of integrity constraints that are part of the given database schema;
  • mutation requests to execute multiple create/update operations with transactional guarantees (ACID);
  • automatic addition of history metadata which can be accessed in queries (creation date, last update date);
  • operations to query the database starting from a selected node that can be identifed based on its ID or based on key properties;
  • operations to query the database starting from a list of nodes of the same type;
  • queries on lists of nodes may apply filter conditions and paging;
  • queries may retrieve properties of nodes, as well as properties of edges;
  • queries may traverse any given edge in either direction.

For the definition of the graph database schema (which is the only input needed for woo.sh), we have developed an approach that repurposes the GraphQL schema definition language as a language to define schemas for graph databases. As the DBMS used by a woo.sh-generated GraphQL server, woo.sh currently supports ArangoDB. Support for other systems may be added in the future.

Try it out

woo.sh comes with a simple example for demonstrating its use. The database schema of this example defines a simple graph database with data related to the Star Wars universe (as adapted from the GraphQL learning material). Using the demo requires a running instance of ArangoDB.

$ sh ./woo.sh --input example/db-schema/ \
              --output ./generated-example-server \
              --config example/config.yml \
              --driver arangodb \
              --custom-api-schema example/custom-api-schema.graphql \
              --custom-resolvers example/custom-resolvers.js

$ npm server.js

Waiting for ArangoDB to become available at http://localhost:8529
ArangoDB is now available at http://localhost:8529
Database dev-db deleted: true
Database 'dev-db' created
Collection 'Droid' created
Collection 'Species' created
Collection 'Planet' created
Collection 'Human' created
Collection 'Starship' created
Edge collection 'FriendsEdgeFromDroid' created
Edge collection 'HomeWorldEdgeFromDroid' created
Edge collection 'SpeciesEdgeFromDroid' created
Edge collection 'OriginEdgeFromSpecies' created
Edge collection 'StarshipsEdgeFromHuman' created
Edge collection 'FriendsEdgeFromHuman' created
Edge collection 'HomeWorldEdgeFromHuman' created
Edge collection 'SpeciesEdgeFromHuman' created
GraphQL service ready at: http://localhost:4000/

Tests

API/driver functionality

Test for API and driver functionality can be found in graphql-server/tests.

To run them first Use the example above for the Star Wars schema, end then run graphql-server\tests\client-api-tests

Directives/Constrains

Tests for directives checking can be found in graphql-server/tests.

To run them first run directives-tests-generator.sh to generate schema, resolver, driver and server as usual. Then start server.js in graphql-server\tests\generated-directives-tests-server (make sure DISABLE_DIRECTIVES_CHECKING is not set to true), and finally run directives-tests.js

woo.sh's People

Contributors

dependabot[bot] avatar epii-1 avatar hartig avatar keski avatar rcapshaw avatar

Stargazers

 avatar  avatar

Watchers

 avatar  avatar  avatar

Forkers

cuulee huanyu-li

woo.sh's Issues

query by id

This issue covers Section 2 of issue #1.

  • Implementation added
  • Tests created
  • All tests passed

add support for OBDA

Various data integration challenges may not be easily solved using only a GraphQL schema. Some of these tasks may be addressed using the general approach of custom queries. However, some tasks may instead be addressed using ontologies in a semantic layer. For example, the GraphQL schema cannot easily represent hierarchies between types, or support dynamic views of data. Ontologies, on the other hand, could support arbitrary-depth type hierarchies, ontology-based querying, simplified graph views, inferencing, and mappings to other data sources.

As a first step towards supporting ontology based data access (OBDA), a new module should be created which can generate a GraphQL schema and the corresponding resolver functions based on an OWL ontology and an R2RML mapping file. The generated files should then be integrated into the main woo.sh pipeline. Details remain to be decided.

warnings vs. exceptions

This is a ticket to discuss when we want to generate exceptions or warnings for cases where the input schema is not formatted as expected.

Current cases to be decided on are:

  1. One or more types in the input schema already have an ID field.
  2. A query type is already defined in the input schema.
  3. A mutation type is already defined in the input schema.
  4. A type that is about to be generated already exists.
  5. A field to be added to a type already exists.

Empty brackets are not supported by the GraphQL Library

When preparing build 1.7.0, I ran into an issue with the updated schema. In particular, the Job-related changes which introduce types without fields cause the generation of the API schema to fail. It looks like the library we use to parse the schemas doesn't like the use of empty curly brackets "{}". Here's what I've found.

When running woo.sh as suggested with the following command:

woo.sh/woo.sh --input graphql-schema/ \
              --output mediator-service/graphql-server \
              --config mediator-service/resources/spirit-api-config.yml \
              --driver arangodb \
              --custom-api-schema mediator-service/resources/spirit-custom-api-schema.graphql \
              --custom-resolvers mediator-service/resources/spirit-custom-resolvers.js

I get the following error:

Traceback (most recent call last):
  File "generator.py", line 287, in <module>
    cmd(parser.parse_args())
  File "generator.py", line 38, in cmd
    schema = build_schema(schema_string)
  File "/home/riley/anaconda3/envs/spirit/lib/python3.6/site-packages/graphql/utilities/build_ast_schema.py", line 444, in build_schema
    experimental_fragment_variables=experimental_fragment_variables,
  File "/home/riley/anaconda3/envs/spirit/lib/python3.6/site-packages/graphql/language/parser.py", line 104, in parse
    return parse_document(lexer)
  File "/home/riley/anaconda3/envs/spirit/lib/python3.6/site-packages/graphql/language/parser.py", line 158, in parse_document
    definitions=many_nodes(lexer, TokenKind.SOF, parse_definition, TokenKind.EOF),
  File "/home/riley/anaconda3/envs/spirit/lib/python3.6/site-packages/graphql/language/parser.py", line 1166, in many_nodes
    append(parse_fn(lexer))
  File "/home/riley/anaconda3/envs/spirit/lib/python3.6/site-packages/graphql/language/parser.py", line 172, in parse_definition
    return parse_type_system_definition(lexer)
  File "/home/riley/anaconda3/envs/spirit/lib/python3.6/site-packages/graphql/language/parser.py", line 555, in parse_type_system_definition
    return func(lexer)
  File "/home/riley/anaconda3/envs/spirit/lib/python3.6/site-packages/graphql/language/parser.py", line 747, in parse_interface_type_definition
    fields = parse_fields_definition(lexer)
  File "/home/riley/anaconda3/envs/spirit/lib/python3.6/site-packages/graphql/language/parser.py", line 679, in parse_fields_definition
    if peek(lexer, TokenKind.BRACE_L)
  File "/home/riley/anaconda3/envs/spirit/lib/python3.6/site-packages/graphql/language/parser.py", line 1163, in many_nodes
    nodes = [parse_fn(lexer)]
  File "/home/riley/anaconda3/envs/spirit/lib/python3.6/site-packages/graphql/language/parser.py", line 688, in parse_field_definition
    name = parse_name(lexer)
  File "/home/riley/anaconda3/envs/spirit/lib/python3.6/site-packages/graphql/language/parser.py", line 147, in parse_name
    token = expect_token(lexer, TokenKind.NAME)
  File "/home/riley/anaconda3/envs/spirit/lib/python3.6/site-packages/graphql/language/parser.py", line 1077, in expect_token
    lexer.source, token.start, f"Expected {kind.value}, found {token.kind.value}"
graphql.error.syntax_error.GraphQLSyntaxError: Syntax Error: Expected Name, found }

GraphQL request:947:16
946 | """
947 | interface Job {
    |                ^
948 | }

So, my first thought was to remove empty brackets from the schema (replacing the schema line mentioned above with just interface Job, among others). This seemed to work until the pipeline generated the input types, where it threw the same error:

Traceback (most recent call last):
  File "generator.py", line 287, in <module>
    cmd(parser.parse_args())
...
  File "/home/riley/anaconda3/envs/spirit/lib/python3.6/site-packages/graphql/language/parser.py", line 1077, in expect_token
    lexer.source, token.start, f"Expected {kind.value}, found {token.kind.value}"
graphql.error.syntax_error.GraphQLSyntaxError: Syntax Error: Expected Name, found }

GraphQL request:186:40
185 | 
186 | extend input _InputToCreateScraperJob {
    |                                        ^
187 | }

@hartig @keski @epii-1 How should we proceed? It seems like simply editing the schema files is not enough (else I would have put this ticket elsewhere). Instead, we may need to strip away empty brackets from types with no fields and not generate input types with empty brackets.

support querying for nodes based on keys (rather than IDs)

Currently, for each type of nodes (as defined by an object type in the SDL), we are creating two query operations: one of which can be used to retrieve a single node based on the ID of that node and the other can be used to retrieve a list of nodes. The aim of this issue is to add a third form of query operations; namely, one that allows users to retrieve a single node based on key attributes as specified by the @key directive (see Sec.3.2 in http://olafhartig.de/files/HartigHidders_GRADES2019_Preprint.pdf).

As a simple example, consider the following definition of a Property Graph schema.

type User  @key(fields:["login"])  {
   login: String! @required
   nicknames: [String!]!
}

The @key directive in this example specifies that the login property of User nodes must be unique (i.e., every User node must have a different value for this property). Hence, login is a key attribute based on which we can uniquely identify any particular User node, which we may also leverage for retrieving nodes via queries in the generate API schema. Therefore, this issue is to extend the tool to generate for every such key an additional query operation in the generated API schema. For the example, this additional query operation would be defined as follows.

type Query {
   userByKey( key: _KeyForUser): User
   # ... other query operations here ...
}

input _KeyForUser {
   login: String!
}

With this addition it is now possible to write the following query.

query {
   userByKey( key: {login: "hartig"} ) {
      nicknames
   }
}

A few additional remarks:

  • If a key is a composite key, consisting of multiple properties (field names), then these properties must all be in the new input type (that is, the input type named by the pattern _KeyForXYZ).
  • If a node type has multiple keys, then it is okay for the moment to consider only the first of these keys.

Remove the option to update edges in node-update operations

Currently, it is possible to update the (outgoing) edges of a node within the corresponding node-update operation. For instance, it is possible to do the following.

mutation {
   updateBlogger( id: "56565", data: {
      name: "Robert",
      blogs: [
         { connect: "41" },
         { connect: "21" }
      ]
   } ) {
      id
   }
}

What this does is to remove (!) all existing edges from blogger 56565 to blogs and replace them by two new edges that connect the blogger to blogs 41 and 21, respectively. This behavior is not very intuitive and may cause a lot of confusion, and it is also problematic in terms of the automatically maintained lastUpdateDate field (as discussed in #63).

I also do not see any other way of defining a more intuitive behavior for this.

Consequently, I am proposing to remove this option altogether. Edges can still be added by the edge-creation operations and removing them, as well as updating their annotations will become available as per #36 and #35, respectively.

@keski @epii-1 Any opinions?

Some configurations are not reflected in the drivers.js

Some configurations are not only applicable for the generation of api-schema, resolvers and which driver to select etc.. Some are also relevant when actually running the driver (as they are setup now).
Currently field_for_creation_date and field_for_last_update_date are both expected to always be set to true in the driver, and actually setting them to false would lead to errors when creating objects etc., as the driver would try to insert fields that does not exists.

How to solve this might not be obvious. My suggestions would be to either keep keep the relevant configurations as actually values in the drivers file(s), or have lines in the driver(s) files that are added/removed dynamically during generations depending on the configurations.

I should have realized this earlier....

@hartig @keski @rcapshaw

mixing of parts from the DB schema and API schema

The current generated schema mixes parts from the DB schema and parts from the generated API schema. More precisely, the argument definitions in this field definition may occur only in the DB schema, but they should be removed when generating the API schema. For example, the DB schema:

type Person {
    marriedTo(since: Date!): Person
}

should in the API schema be rendered as (note that the arguments are accessed via the added fields for edge types):

type Person {
    marriedTo: Person
}

@hartig

Bug in API schema generator related to the uniqueForTarget constraint

The unique-for-target constraints that can be specified by adding the @uniqueForTarget directive is not correctly captured when generating the API schema.

As an example, consider the following part of a DB schema:

type Blogger {
  blogs: [Blog]  @uniqueForTarget
}

type Blog {
  text: String!  @required
}

The directive @uniqueForTarget in this example says that every Blog node can have at most one incoming blogs edge from some Blogger. Consequently, when we create the corresponding API schema and add a field to Blog that can be used to traverse these edges in the reverse direction, we expect this added field to be of type Blogger and not [Blogger] (list of Blogger objects). In other words, we expect the API schema to look as follows.

type Blogger {
  blogs: [Blog]
}

type Blog {
  text: String!
  _blogsFromBlogger: Blogger
}

However, in contrast to this expected API schema, our generator currently produces the following.

type Blogger {
  blogs: [Blog]
}

type Blog {
  text: String!
  _blogsFromBlogger(filter: _FilterForBlogger): [Blogger]
}

This issue needs to be fixed.

Approach to auto-generate the GraphQL API schema from the DB schema

In the Athens meeting we agreed that

  1. we will use a GraphQL API for the SPIRIT components to access the content database (DB), and
  2. we will use the GraphQL schema language for defining the schema of the content DB.

For point 1 (the GraphQL API) we need a GraphQL schema. Notice that, conceptually, this schema is something else than the schema we are creating for the content DB as per point 2. More precisely, while the schema for the content DB defines what exactly the objects look like that we store in the content DB, the schema for the API defines how these objects can be queried via the GraphQL API (i.e., what data can be requested for these objects) and how these objects can be inserted and modified via the GraphQL API. Essentially, the schema for the GraphQL API needs to contain some more things than what we have in the schema for the DB. A natural question at this point is: How do we get to this schema for the API?

The answer is to generate this schema automatically from the DB schema! The advantage of this approach is that we do not need to do any manual work for creating the API schema. Instead, that schema is simply generated by pushing a button (or, calling a command-line tool, to be more precise ;) Moreover, if we later extend or modify the DB schema, we can easily generate an extended API schema that reflects the changes of the DB schema. Now, the question is: How is the API schema generated from the DB schema?

We (LiU) will define an approach to generate the API schema from the DB schema, and we will develop the tool that implements this approach. In the following, I provide an overview of the approach.

The idea of the approach to generate the API schema from the DB schema is to copy the DB schema into a new file and, then, extend the schema in this new file with all the additional things needed for the API schema. These additional things needed are:

  1. an ID field in every object type that enables the GraphQL queries to access the system-generated identifier of each object,
  2. a query type that specifies the starting points of queries sent to the GraphQL API,
  3. additional fields in the object types that enable the GraphQL queries to traverse relationships between the objects in the reverse direction,
  4. additional fields in the object types that enable the GraphQL queries to access the data associated with these relationships, and
  5. a mutation type and corresponding input types that specify how data can be inserted and modified via the GraphQL API.

1. ID Fields

When inserting a data object into the database, the database management system (ArangoDB in our case) generates an identifier for it. While these identifiers do not need to be part of the DB schema, they should be contained in the schema for the GraphQL API so that they can be requested in GraphQL queries (and, then, used later in subsequent queries). Therefore, when extending the DB schema into the schema for the GraphQL API, each object type is augmented with a field named ID whose value type is ID!.

2. Query Type

Every GraphQL schema for a GraphQL API must have one special type called the query type. The schema for the DB does not need such a query type and, in fact, it should not contain one. The purpose of the query type is to specify the possible starting points of any kind of query that can be sent to the API. For instance, consider the following snippet of a GraphQL API schema which defines the query type of the corresponding API.

type Query {
    Investigation(ID:ID!): Investigation
}

Based on this query type, it is (only) possible to write queries (API requests) that start from an Investigation object specified by a given ID. For instance, it is possible to write the following query.

query {
    Investigation(ID:371) {
        Title
        Description
        Authorization {
            SearchPurpose
            Necessity
        }
    }
}

However, with a query type like the one above, it would not be possible to query directly for, say, an Authorization object specified by its ID.

Now, when extending the DB schema into the API schema, the plan is to generate a query type that contains two fields (i.e., starting points for queries) for every object type in the DB schema: one of these fields can be used to query for one object of the type based on the ID of that object, and the second field can be used to access a paginated list of all objects of the corresponding type. The list is paginated, which means that it can be accessed in chunks.

For example, for the Investigation type that we have in our DB schema, the generated query type would contain the following two fields.

    Investigation(ID:ID!): Investigation
    ListOfInvestigations(first:Int after:ID): ListOfInvestigations!

The additional type called ListOfInvestigations that is used here will be defined as follows.

type ListOfInvestigations {
    totalCount: Int
    isEndOfWholeList: Boolean
    content: [Investigation]
}

Then, it will be possible to write queries such as the following.

query {
    ListOfInvestigations(first:10 after:371) {
        totalCount
        isEndOfWholeList
        content {
            Title
            Description
            Authorization {
                SearchPurpose
            }
        }
    }
}

3. Additional Fields For Traverssal

In the DB schema, each type of relationships (edges) between objects of particular types is defined only in one of the two related object types. For instance, consider the two object types Investigation and Authorization whose definition in the DB schema looks as follows.

type Investigation {
    UserID: ID!
    Title: String!
    Description: String!
    CaseNumber: String!
    Authorization: Authorization
    Searches: [Search]
    Created: Date!
    Hide: Boolean!
}

type Authorization {
    SearchPurpose: String!
    Necessity: [Necessity]
    Proportionality: [Proportionality]
    TrainedAndAuthorized: Boolean  
    DarkWeb: Boolean!
    AuthBy: Authority!
    Created: Date!
}

Notice that the relationship (i.e., the possible edges) between Investigation objects and Authorization objects are defined only in the definition of the type Investigation (see the field named Authorization) but not in the type Authorization. Specifying every edge type only once is sufficient for the purpose of defining the schema of a (graph) database. However, it is not sufficient for supporting bidirectional traversal of these edges in GraphQL queries. Hence, the schema for the API needs to mention possible edges twice; that is, in both of the corresponding object types. For the aforementioned example of the relationships between Investigation objects and Authorization objects, the API schema, thus, needs to contain an additional field in the type Authorization such that this field can be used to query from an Authorization object to the Investigation objects that point to it via their Authorization fields. Hence, when extending the aforementioned part of DB schema into the schema for the GraphQL API, the definition of the Authorization type will be extended as follows.

type InvAuthorization {
    ID: ID!
    SearchPurpose: String!
    Necessity: [Necessity]
    Proportionality: [Proportionality]
    TrainedAndAuthorized: Boolean  
    DarkWeb: Boolean!
    AuthBy: Authority!
    Created: Date!
    Investigation: [Investigation]
}

Observe that the value type of the added field named Investigation is a list of Investigation objects. This is because, according to the DB schema, multiple different Investigation objects may point to the same Authorization object; i.e., the relationship between Investigation objects and Authorization objects is a many-to-one relationship (N:1). Therefore, from an Authorization object, we may come to multiple Investigation objects.

Perhaps this was not the intention and, instead, the relationship between Investigation objects and Authorization objects was meant to be a one-to-one relationship. This could have been captured by adding the @uniqueForTarget directive to the field named Authorization in the DB schema (as described in the text before Example 7 of http://blog.liu.se/olafhartig/documents/graphql-schemas-for-property-graphs/). Assuming that there would be such a @uniqueForTarget directive, then the new field named Investigation that is added when extending the DB schema into the API schema would be defined differently:

type InvAuthorization {
    ID: ID!
    SearchPurpose: String!
    Necessity: [Necessity]
    Proportionality: [Proportionality]
    TrainedAndAuthorized: Boolean  
    DarkWeb: Boolean!
    AuthBy: Authority!
    Created: Date!
    Investigation: Investigation
}

This example demonstrates that the exact definition of the fields that are added when extending the DB schema into the API schema depends on the constraints that are captured by directives in the DB schema. To elaborate a but further on this point, let us assume that the aforementioned field named Authorization in the DB schema would additionally be annotated with the @requiredForTarget directive (in addition to the @uniqueForTarget directive). In this case, the extension of the type Authorization for the API schema would look as follows (notice the additional exclamation mark at the end of the value type for the new Investigation field).

type InvAuthorization {
    ID: ID!
    SearchPurpose: String!
    Necessity: [Necessity]
    Proportionality: [Proportionality]
    TrainedAndAuthorized: Boolean  
    DarkWeb: Boolean!
    AuthBy: Authority!
    Created: Date!
    Investigation: Investigation!
}

4. Additional Fields and Types For Edges

Edges in a Property Graph database may have properties (key-value pairs) associated with them. When defining the DB schema, these properties can be defined as field arguments as demonstrated in the following snippet of a DB schema.

type Blogger {
    Name: String!
    Blogs(certainty:Int! comment:String): [Blog]  @uniqueForTarget @requiredForTarget
}

type Blog {
    Title: String!
    Text: String!
}

By this definition, every edge from a Blogger object to a Blog object has a certainty property and, optionally, it may have a comment property.

Field arguments such as certainty and comment would have a different meaning when used in a schema for the GraphQL API and, thus, they have to be removed from the field definitions when extending the DB schema into the API schema. Hence, after removing the field arguments (and adding the aforementioned ID fields and the fields for traversing edges in the opposite direction), the API schema for the aforementioned DB schema would look as follows.

type Blogger {
    ID: ID!
    Name: String!
    Blogs: [Blog]  @uniqueForTarget @requiredForTarget
}

type Blog {
    ID: ID!
    Title: String!
    Text: String!
    Blogger: Blogger!
}

Although we have to remove the field arguments from the fields that define edge types in the DB schema, we may want to enable GraphQL queries to access the values of the edge properties that these edges of these types have. For instance, we may want to query for the certainty of edges between bloggers and blogs. To this end, the edges have to be represented as objects in the GraphQL API. Hence, it is necessary to generate an object type for each type of edges and integrate these object types into the schema for the API. For instance, for the edges between bloggers and blogs, an object type called BlogsEdgeFromBlogger will be generated and access to objects of this new type will be integrated into the schema by adding a new field to the Blogger type and to the Blog type, respectively.

type Blogger {
    ID: ID!
    Name: String!
    Blogs: [Blog]  @uniqueForTarget @requiredForTarget
    OutgoingBlogsEdges: [BlogsEdgeFromBlogger]
}

type Blog {
    ID: ID!
    Title: String!
    Text: String!
    Blogger: Blogger!
    IncomingBlogsEdgeFromBlogger : BlogsEdgeFromBlogger!
}

type BlogsEdgeFromBlogger {
    ID: ID!
    source: Blogger!
    target: Blog!
    certainty:Int!
    comment:String
}

Given this extension, it is now possible to write GraphQL queries that access properties of the edges along which they are traversing. The following query demonstrates this option.

query {
    Blogger(ID:3991) {
        Name
        OutgoingBlogsEdges {
            certainty
            target {
                Title
                Text
            }
        }
    }
}

5. Mutation Type and Corresponding Input Types

In addition to the aforementioned query type, another special type that a GraphQL API schema may contain is the mutation type. The fields of this type specify how data can be inserted and modified via the GraphQL API. For instance, the following snippet of a GraphQL API schema defines a mutation type.

type Mutation {
    setTitleOfInvestigation(ID:ID! Title:String!): Investigation
}

Given this mutation type, it is possible to modify the title of an Investigation object specified by a given ID; the result of this operation is defined to be an Investigation object (we may assume that this is the modified Investigation, which may then be retrieved as the response for the operation).

Now, when extending the DB schema into the API schema, the plan is to generate a mutation type that contains three operations for every object type in the DB schema and another three operations for every edge type. The three operations for an object type XYZ are called createXYZ, updateXYZ, and deleteXYZ; as their names suggest, these operations can be used to create, to update, and to delete an object of the corresponding type, respectively. In the following, we discuss the mutation operations in more detail.

5.1 Creating an Object

Consider the aforementioned object type Investigation of our DB schema. The create operation for Investigation objects will be defined as follows.

    createInvestigation(data:DataToCreateInvestigation!): Investigation

The value of the argument data is a complex input object that provides the data for the Investigation object that is to be created. This input object must be of the type DataToCreateInvestigation. This input type, which will be generated from the object type Investigation of the DB schema, will be defined as follows.

input DataToCreateInvestigation {
    UserID: ID!
    Title: String!
    Description: String!
    CaseNumber: String!
    Authorization: DataToConnectAuthorizationOfInvestigation
    Searches: [DataToConnectSearchesOfInvestigation]
    Created: Date!
    Hide: Boolean!
}

input DataToConnectAuthorizationOfInvestigation {
    connect: ID
    create: DataToCreateAuthorization
}

input DataToConnectSearchesOfInvestigation {
    connect: ID
    create: DataToCreateManualSearch
}

input DataToConnectScheduleSearchesOfInvestigation {
    connect: ID
    create: DataToCreateSearch
}

Notice that all fields that are mandatory in Investigation are also mandatory in DataToCreateInvestigation (and optional fields remain optional). Moreover, fields whose value type in Investigation is a scalar type (or a list thereof) have the same value type in DataToCreateInvestigation. In contrast, fields that represent outgoing edges in Investigation have a new input type that can be used to create the corresponding outgoing edge(s). This can be done in one of two ways: either by identifying the target node of the edge via the connect field or by creating a new target node via the create field.

5.2 Updating an Object

The update operations for Investigation objects will be defined as follows.

    updateInvestigation(ID:ID! data:DataToUpdateInvestigation!): Investigation

The Investigation object to be updated can be identified by the argument ID. The value of the argument data in this case is another input type that provides values for the fields to be modified. This input type is defined as follows.

input DataToUpdateInvestigation {
    UserID: ID
    Title: String
    Description: String
    CaseNumber: String
    Authorization: DataToConnectAuthorizationOfInvestigation
    ManualSearches: [DataToConnectManualSearchesOfInvestigation]
    ScheduleSearches: [DataToConnectScheduleSearchesOfInvestigation]
    Created: Date
    Hide: Boolean
}

Notice that all fields in this input type are optional to allow users to freely choose the fields of the Investigation object that have to be updated. For instance, to update the UserID and the Title of the Investigation object with the ID 371 we may write the following.

mutation {
    updateInvestigation(
           ID:371
           data: {
              UserID:87
              Title:"Five Orange Pips"
           }
    ) {
        ID
        UserID
        Title
      }

Updates like this override the previous value of the updated fields. In the case of outgoing edges this means that new edges replace all previously existing edges (without removing the target nodes of replaced edges). If you want to add edges instead, use the connect operations described below.

5.3 Deleting an Object

The delete operations for Investigation objects will be defined as follows.

    deleteInvestigation(ID:ID!): Investigation

The argument ID can be used to identify the Investigation object to be deleted.

Notice that deleting an object implicitly deletes all incoming and all outgoing edges of that object.

5.4 Creating an Edge

Consider the types of edges that represent the aforementioned relationship between bloggers and blogs. In the DB schema, this type of edges is defined implicitly by the field definition Blogs in object type Blogger. The create operation for these edges will be defined as follows.

    createBlogsEdgeFromBlogger(data:DataToCreateBlogsEdgeFromBlogger!): BlogsEdgeFromBlogger

The new input type for this operation, DataToCreateBlogsEdgeFromBlogger, will be generated as follows.

input DataToCreateBlogsEdgeFromBlogger {
    sourceID: ID!   # assumed to be the ID of a Blogger object
    targetID: ID!   # assumed to be the ID of a Blog object
    certainty:Int!
    comment:String
}

5.5 Updating an Edge

Update operations for edges will be generated only for the types of edges that have edge properties. The edges that represent the aforementioned relationship between bloggers and blogs are an example of such edges. The update operation that will be generated for these edges is:

    updateBlogsEdgeFromBlogger(ID:ID! data:DataToUpdateBlogsEdgeFromBlogger!): BlogsEdgeFromBlogger

The argument ID can be used to identify the BlogsEdgeFromBlogger object that represents the edge to be updated.
The new input type for this operation, DataToUpdateBlogsEdgeFromBlogger, will be generated as follows.

input DataToUpdateBlogsEdgeFromBlogger {
    certainty:Int
    comment:String
}

#### 5.6 Deleting an Edge
The delete operations for the edges between bloggers and blogs will be defined as follows.

deleteBlogsEdgeFromBlogger(ID:ID!): BlogsEdgeFromBlogger


The argument `ID` can be used to identify the `BlogsEdgeFromBlogger` object that represents the edge to be deleted.

input types to create objects

This issue covers parts of Section 5 of issue #1.

Add input types to create objects.

  • Implementation added
  • Tests created
  • All tests passed

fork gaphql-js and use as npm dependency

The current graphql-server implementation patches part of the graphql-js npm library in order to support multiple transactions. This approach is not very clean and is difficult to maintain.

This ticket is about forking the graphql-js and extending it so that our patched library can be used as a regular npm dependency as:

"graphql": "git://github.com/LiUGraphQL/graphql-js.git#npm"

The forked library should be maintained separately and extend (not change) the current functionality of the graphql-js. To this end, the current approach to transactions (and the corresponding functions (create, createEdge, etc.) will have to be updated to return promises, since this is the expected result in graphql-js, and not use the async-wait pattern.

listOf queries not generated for interfaces

listOfX query operations are generated for both types and interfaces in the API generator. However, in the template used by the resolver-generator this is missing for interfaces. This means that e.g. listOfJobs will always return none. The underlying code already supports interfaces for listOfX queries (including filters). This is therefore a minor bug that is easily fixed by including the following in the resolver template:

    % for interface_name in data['interfaces']:
        listOf${interface_name}s: async (parent, args, context, info) => await tools.getList(args, info),
    % endfor

Calling generator.py with --help gives me a syntax error

If I execute

python3 generator.py --help

... I get the following error message:

  File "generator.py", line 29
    files.append(f'{args.input}/{filename}')
                                          ^
SyntaxError: invalid syntax

The same happens when I try to execute the example command:

$ python3 generator.py \
      --input resources/schema.graphql \
      --output api-schema.graphql \
      --config resources/config.yml

add id to types

This issue covers Section 1 of issue #1.

  • Implementation added
  • Tests created
  • All tests passed

Annotating edges in mutations

We need a strategy to deal with annotations on fields during mutations. This is supported in the spec with regard to types for edges and their corresponding operations. However, the use of arguments on input type fields is not supported in GraphQL, and if an object has an edge annotation marked as required/non-null, it can currently not be created.

There are a couple of alternatives for this as I see it (naming may vary). Assume the following incomplete schema:

type Person {
   id: ID!
   name: String!
   knows(since: Date!, description: String): [Person]
}

mutation {
   createPerson(data: _InputToCreatePerson)
}

input _InputToCreatePerson {
   name: String!
   knows: [_InputToConnectKnowsOfPerson]
}
  1. We could provide additional fields for the annotations in _InputToConnectX, e.g.:
input _InputToConnectKnowsOfPerson {
   connect: ID
   create: _InputToCreatePerson
   since: Date!
   description: String
}
  1. Another alternative would be to define a new type that defines fields for annotations on the edge, e.g.:
input _InputToConnectKnowsOfPerson {
   connect: ID
   create: _InputToCreatePerson
   annotation: _InputToAnnotateKnowsOfPerson! # when should this field be marked as non-null?
}

input _InputToAnnotateKnowsOfPerson {
   since: Date!
   description: Date
}

The benefit of using alt 1 is that we don't have to add any additional input types, but simply extend the current _InputToConnectX. This is also close to the pattern for _InputToCreateXEdgeFromY in the spec. The benefit of using alt 2 is that the field names for all _InputToConnectX input types are fixed (similar to providing a single input type as data during mutations). If we go with alternative 2, we may also consider using it for the _InputToCreateXEdgeFromY, e.g.

input _InputToCreateKnowsEdgeFromPerson {
   sourceID: ID!
   targetID: ID!
   annotations: _InputToAnnotateKnowsOfPerson! # when should this field be marked as non-null?
}

@hartig

Enforcing constraint in all relevant mutation operations

Collecting what remains of #47, #48, #49 and #50 here to easier keep track of what remains.
It should be enough to call addFinalDirectiveChecksForType from all relevant functions implementing the operations, once they are implemented.

  • create-object operations
  • update-object operations (With the removal of adding/deleting edges through object updates #67, this is no longer needed).
  • delete-object operations
  • create-edge operations
  • delete-edge operations
  • update-edge operations (Not needed as the directives are only relevant for fields of object types, and updateEdge can only update scalar and enum fields)

mutation for creating objects

This issue covers parts of Section 5 of issue #1.

Add mutation fields to create objects.

  • Implementation added
  • Tests created
  • All tests passed

Collection locks are misbehaving when querying

@keski I found a strange bug while trying to implement edge deletions.
I run the following mutations (in different transactions):

create object1
create object2
# Query nr1 of object1
create edgeFromObject1ToObject2
# Query nr2 of object1
delete edgeFromObject1ToObject2
# Query nr3 of object1

This works just fine while ran like this and queried once at any location.
However, if I start querying at multiple locations during these mutations strange things starts to happen:
If I query object1 at location 1, neither query nr2 nor query nr3 will contain the edge create in the create edge operation.
If I skip query nr1 while still doing query 2 and 3, both query nr3 and nr2 will contain the edge.
If I skip both 1 and 2, 3 will not contain the edge.

So to me it looks like the query result is decided the first time an object/collection is queried and is there after static.
I suspect that query operations does not release their read locks and therefor prevents the later mutations.
At the same time I can not see that we ever explicitly get any locks for the get operations.

See the edge_delete branch for testing.
I also have an example (using the spirit schema) I can dm you to test it out.
But you should be able to achieve similar results on the current dev. build by just skipping the delete edge operation and the third query.

add tool for creating graphql server

This tool should be extracted from the current mediator-service in GitLab. The goal is to be able to generate a the files necessary for automatically setting up a basic GraphQL server, based on a given GraphQL API schema and the corresponding resolver functions. The backend of the generated server should be configurable (i.e., not hard coded). It should be possible, starting from a GraphQL DB schema, to set up a working GraphQL server configured for a particular database backend.

Importantly, everything included in this tool MUST be unrelated to anything SPIRIT specific.

pass the original API schema to the driver

We need to provide a distinction between the generated API GraphQL schema, from which collections/tables are generated in the driver.js, and the schema used by the GraphQL server, which includes the custom extensions. To this end, we need pass the original schema to the drivers, which should then be used as the schema when setting up the database.

reverse links for traversal

The spec doesn't specify how reverse links are to be added when a field has an interface as its type. Below is one possible approach, which introduces a new edge for each implementing type. Internally, this means that, in the case of an interface type, ArangoDB requests need to traverse all edge collections generated for the types implementing an interface.

interface Char {
   ...
}

type A {
   value: Char
}

type B implements Char {
   ...
}

type C implements Char {
   ...
}

After introduction of inverse links, the extended types would be something like:

type B implements Char {
   valueOfA: [A]
   ...
}

type D implements Char {
   valueOfA: [A] # edge name in ArangoDB: EdgeToConnectValueOfAOfD
   ...
}

@hartig

query by type

This issue covers Section 2 of issue #1.

This should cover also pagination.

  • Implementation added
  • Tests created
  • All tests passed

handle interfaces in mutations

The spec is not clear about what to do when generating mutations from types that use interfaces in one or more fields. Since it is not possible to generate an input type based on an interface, this is a special case. The input type used for creating an image in the example below should support any type that implements the Job interface (ThirdPartySearchJob or CrawlingJob):

interface Job {
   ...
}

type ThirdPartySearchJob implements Job {
   ...
}

type CrawlingJob implements Job {
   ...
}

type Image implements Content{
    ...
    job: Job!
}

input DataToCreateImage {
   ...
   job: ???
}

An option would be to provide separate fields for connecting/creating types implementing the interface. Example:

input DataToCreateImage {
   ...
   job: DataToConnectJobOfImage!
}

input DataToConnectJobOfImage {
  id : ID
  dataToCreateThirdPartySearchJob: DataToCreateThirdPartySearchJob
  dataToCreateCrawlingJob: DataToCreateCrawlingJob
}

@hartig Do you have any thoughts on this?

creationDate and lastUpdateDate is not enforced/kept properly for edges

Currently creationDate for edges is only enforced when the edge is created from createEdge not when the edge is created by create-object calls (quite easy to fix).
What is more complicated to fix is the lastUpdateDate. As we currently recreate edges on an update-object call (if we update those edges at all), we would then get new creationDates for those edges if fixed in similar fashion as to when creating objects.
This needs to be changed if creationDate and lastUpdateDate are to behave as expected when used for queried etc..
One alternative would be to prevent edge updates from object updates, and only allow edges to be updated from actual edge update calls, which is currently not implemented.

@hartig @keski @rcapshaw

filter on returned types

It should be possible to filter on edges using a similar filter pattern as for list. The pattern supported should be as follows:

query {
   listOfPersons(filter: { name: { like: 'John%'} }){
      name
      knows(filter: { name: { like: 'Mary%'} }){
         name
      }
   }

All fields referencing an object type should to be extended with a filter argument for that type.
@hartig

Develop branch gets error during create and update operations

@keski I'm getting error while running client-test with mediators generated from the develop branch:

graphQLErrors: [
    {
      message: "ArangoError: AQL: bind parameter 'value2' was not declared in the query (while parsing)",
      locations: [Array],
      path: [Array],
      extensions: [Object]
    }
  ],

The error seem to always be related to value2. Is the parameters (other than the first) not getting defined correctly? Or do I have any dependencies out of sync or something?

PS: I have to run let inputToCreate = tools.makeInputToCreate(t, schema, 4, true); with 4 or above, else I get another error related to dummy fields?

Adding outgoing and inbound edge fields for types.

@hartig
I now saw https://github.com/LiUGraphQL/woo.sh/wiki/Overview-of-the-Approach#4-additional-fields-and-types-for-edges (was too focused on the mutations part of edge annotations to take note of this earlier).
Is this still the intended way to have edge parameters/annotations available to querying? (I see some downsides with this. But all other approaches I can think of are no better.)
If so, types needs to be extended with these fields before #31 and #32 can be implemented.
If the answer is no, we need to decide a desired format for edge property returns in any case.

transaction feature: multiple *dependent* mutation operations within a single mutation request

While the generated servers can already support mutation requests that contain multiple operations that are independent from one another, the purpose of this issue is to also support mutation requests that contain multiple operations that depend on one another. The typical use case for such a functionality is the following: A client may want to create two objects within a single transaction where some value to be used for creating the second object is a value that is auto-generated when creating the first object (e.g., an ID). In terms of the GraphQL request language, this may look like the following:

mutation {
  createXYZ( data: {
        name: "D"
     }
  )
  {
    id @export(as: "idOfD")
    name
  }

  createXYZ( data: {
      name: "C",
      neighbors: [
        { connect: $idOfD }
      ]
    }
  )
  {
    id
    name
  }
}

Notice that this mutation request contains two separate create operations where the second one uses the ID created by the first one.

Notice also that this example uses a special directive called @export, which is necessary to establish some form of variables (like the variable $idOfD in this example). While there is nothing for this in the GraphQL spec, there is a discussion of a related feature for the next version of the spec.

data creation via the reverse edges?

As per #19 (comment), @keski asks the following:

The purpose of the reverse links is primarily to make querying more flexible and expressive. However, do we also want to support data creation via these reverse edges? For example,

type A {
   value: B
}

type B {
   name: String!
   _valueOfA: [A] # Reverse link, 1:N
}

input _InputToCreateA {
   value: _InputToConnectB
}

input _InputToConnectA {
   connect: ID!
   create: _InputToCreateA
}

input _InputToCreateB {
    name: String!
    _valueOfA: _InputToConnectA # do we include this field?
}

input _InputToConnectB {
   connect: ID!
   create: _InputToCreateB
}

Initial version numbering

As part of the overall build process, I need to begin versioning/tagging this repository. While this ticket is mostly just to document that we are now doing this, I do have two questions.

First, it was suggested to make the initial tag 0.0.1. When reading through the page on semantic versioning which GitHub recommends, it is actually suggested to use v0.1.0 in this case. Note the addition of 'v' and the fact that the minor version is now 1. Do we want to follow this standard?

Second, do we want to include release notes as we do for the mediator service or just leave the version as a lightweight tag?

The tag will be applied to this commit: 2ebf233

@hartig @keski

support filtering lists of edges based on edge properties

Once #31 has been addressed, the tool should be extended to also support filtering when retrieving lists of edges.

For instance, consider the example in the corresponding part of the description of the approach. Once #31 has been addressed, we can write the following query.

query {
    Blogger(id:3991) {
        name
        _outgoingBlogsEdges {
            certainty
            target {
                title
                text
            }
        }
    }
}

The purpose of this issue here is to extend the approach such that we may extend the example query as follows.

query {
    Blogger(id:3991) {
        name
        _outgoingBlogsEdges( filter: {certainty:{ _gte: 0.4 }} ) {
            certainty
            target {
                title
                text
            }
        }
    }
}

The schema for this new filter feature should resemble the schema for the existing filter features.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.