How to add sorting for field object of graphql type which refers to different graphql type? - neo4j

I am using Neo4j dB and using pattern comprehension to return the values. I have 2 types Person and Friend:
(p:Person)-[:FRIEND_WITH]->(f:Friend)
Type Person{
id: String
name: String
friends: [Friend]
}
Type Friend{
id: String
name: String
}
type Query {
persons( limit:Int = 10): [Person]
friends( limit:Int = 10): [Friend]
}
What i want to do is to pull the array list of field friends (present in Person Type) in ascending order when the "persons" query executes. For e.g.
{
"data": {
"persons": {
"id": "1",
"name": "Timothy",
"friends": [
{
"id": "c3ef473",
"name": "Adam",
},
{
"id": "ef4e373",
"name": "Bryan",
},
(
"id": "e373ln45",
"name": "Craig",
},
How should I do it ? I researched regarding the sorting, but I did not find anything specific on the array object's sorting when we are using pattern comprehension in neo4j. Any suggestions would be really helpful !

I used the sortBy function of lodash to return the result into an ascending order.
And here is the graphql resolver query:
persons(_, params) {
let query = `MATCH (p:Person)
RETURN p{
.id,
.name,
friends: [(p)-[:FRIEND_WITH]->(f:Friend)) | f{.*}]
}
LIMIT $limit;`;
return dbSession().run(query, params)
.then(result => {
return result.records.map(record => {
let item = record.get("p");
item.friends = sortBy(item.friends, [function(i) {
return i.name;
}]);
return item;
})
})
}

Related

OData GroupBy and Select

Expecting the following example table CustomerOrders
Id
CustomerId
Customer
Product
1
1
Alice
Pizza
2
1
Alice
Pasta
3
2
Bob
Burger
In C# I'm was able to use the following Linq query to produce a nice List<Customer> result with a nested orders collection for every customer:
List<CustomerOrders> queryResult = GetCustomerOrders();
return queryResult
.GroupBy(x => x.CustomerId)
.Select(x => new Customer
{
Id = x.First().CustomrId,
Customer = x.First().Customer,
Orders = x.ToList()
})
.ToList();
Now I want to achive this result directly over an odata query in the client application to get the following JSON result:
[
{
"id": 1,
"customer": Alice,
"orders": [ "Pizza", "Pasta" ]
},
{
"id": 2,
"customer": Bob,
"orders": [ "Burger" ]
}
]
Is there a way to transfer this query in odata?
GroupBy in OData is similar to SQL, only the aggregates and common columns are returned, we lose access to the individual items, so we can return a grouping and a count of the orders, using group by, but not the array of orders.
If your schema has a Customer entity and there is a collection navigation property from Customer to Orders, then we do not need to use grouping at all:
~/Customers?$expand=Orders($select=Product)&$select=Id,Name
The output is structured in a slightly similar manner and should resemble something like this:
{
"#odata.context": "~/$metadata#Customers(Id,Name,Orders(Product))",
"value": [
{
"Id": 1,
"Name": "Alice",
"Orders": [{"Product": "Pizza"},
{"Product": "Pasta"}]
},
{
"Id": 2,
"Name": "Bob",
"Orders": [{"Product": "Burger"}]
}
]
}
A key concept in OData is that the shape of the overall graph should not be modified, it is designed deliberately to always maintain the structure of the Entities that are returned. This means that the definition document is always correct, the only thing missing from this response is the additional fields that were not requested.
If you need the output in the client specifically as mentioned, then you can expose that as a custom function on the controller:
[EnableQuery]
public IQueryable<CustomerSummary> GetCustomersWithOrderSummary()
{
List<CustomerOrders> queryResult = GetCustomerOrders();
return queryResult
.GroupBy(x => x.CustomerId)
.Select(x => new CustomerSummary
{
Id = x.Key,
Customer = x.First().Customer,
Orders = x.Select(o => o.Product)
});
}
If using GroupBy, the closest response we can get is this:
~/CustomerOrders?$apply=groupby((CustomerId,Customer),aggregate($count as Orders))
But here we will return a count of the orders, and not an array of the product values as expected:
{
"#odata.context": "~/$metadata#CustomerOrders(CustomerId,Customer,Orders)",
"value": [
{
"#odata.id": null,
"CustomerId": 1,
"Customer": "Alice",
"Orders": 2
},
{
"#odata.id": null,
"CustomerId": 2,
"Customer": "Bob",
"Orders": 1
}
]
}

Returning tasks for each session in Neo4J (list of lists)

I'm using Neo4J for a mentor platform I'm building and I'm stumped by the following:
Given the following nodes and properties:
Mentor{ login, ... }
Mentee{ login, ... }
Session{ notes, ... }
Task{ complete, name }
And the following associations:
// each session has 1 mentor and 1 mentee
(Mentor)<-[:HAS]-(Session)-[:HAS]->(Mentee)
// each task is FOR one person (a Mentor or Mentee)
// each task is FROM one Session
(Session)<-[:FROM]-(Task)-[:FOR]->(Mentor or Mentee)
What's the best way to query this data to produce an API response in the following shape? Similarly, is this a reasonable way to model the data? Maybe something with coalesce?
{
mentor: { login: '...', /* ... */ },
mentee: { login: '...', /* ... */ },
sessions: [
{
notes,
/* ... */
mentorTasks: [{ id, name, complete }],
menteeTasks: [{ id, name, complete }]
]
I first tried:
MATCH (mentor:Mentor{ github: "mentorlogin" })
MATCH (session:Session)-[:HAS]->(mentee:Mentee{ github: "menteelogin" })
OPTIONAL MATCH (mentor)<-[:FOR]-(mentorTask:Task)-[:FROM]->(session)
OPTIONAL MATCH (mentee)<-[:FOR]-(menteeTask:Task)-[:FROM]->(session)
RETURN
mentor,
mentee,
session,
COLLECT(DISTINCT mentorTask) as mentorTasks,
COLLECT(DISTINCT menteeTask) as menteeTasks
ORDER BY session.date DESC
But that's janky - The mentor and mentee data is returned many times, and it's completely gone if the mentee has no sessions.
This seems more appropriate, but I'm not sure how to fold in the tasks:
MATCH (mentor:Mentor{ github: "mentorlogin" })
MATCH (mentee:Mentee{ github: "menteelogin })
OPTIONAL MATCH (session:Session)-[:HAS]->(mentee)
OPTIONAL MATCH (mentor)<-[:FOR]-(mentorTask:Task)-[:FROM]->(session)
OPTIONAL MATCH (mentee)<-[:FOR]-(menteeTask:Task)-[:FROM]->(session)
RETURN
mentor,
mentee,
COLLECT(DISTINCT session) as sessions
EDIT: Working! thanks to a prompt response from Graphileon. I made a few modifications:
changed MATCH statement so it returns the mentor and mentee even if there are no sessions
sort sessions by date (most recent first)
return all node properties, instead of whitelisting
MATCH (mentor:Mentor{ github: $mentorGithub })
MATCH (mentee:Mentee{ github: $menteeGithub })
RETURN DISTINCT {
mentor: mentor{ .*, id: toString(id(mentor)) },
mentee: mentee{ .*, id: toString(id(mentee)) },
sessions: apoc.coll.sortMaps([(mentor:Mentor)<-[:HAS]-(session:Session)-[:HAS]->(mentee:Mentee) |
session{
.*,
id: toString(id(session)),
mentorTasks: [
(session)<-[:FROM]-(task:Task)-[:FOR]->(mentor) |
task{ .*, id: toString(id(task)) }
],
menteeTasks: [
(session)<-[:FROM]-(task:Task)-[:FOR]->(mentee) |
task{ .*, id: toString(id(task)) }
]
}
], "date")
} AS result
Presuming you would have these data:
You can do something along these lines, with nested pattern comprehensions
MATCH (mentor:Mentor)<-[:HAS]-(:Session)-[:HAS]->(mentee:Mentee)
RETURN DISTINCT {
mentor: {id:id(mentor), name: mentor.name},
mentee: {id:id(mentee), name: mentee.name},
sessions: [(mentor:Mentor)<-[:HAS]-(session:Session)-[:HAS]->(mentee:Mentee) |
{ id: id(session),
name: session.name,
mentorTasks: [(session)<-[:FROM]-(task:Task)-[:FOR]->(mentor) |
{id:id(task), name: task.name}
],
menteeTasks: [(session)<-[:FROM]-(task:Task)-[:FOR]->(mentee) |
{id:id(task), name: task.name}
]
}
]
} AS myResult
returning
{
"mentor": {
"name": "Mentor Jill",
"id": 211
},
"sessions": [
{
"menteeTasks": [
{
"id": 223,
"name": "Task D"
},
{
"id": 220,
"name": "Task C"
},
{
"id": 219,
"name": "Task B"
}
],
"name": "Session 1",
"id": 208,
"mentorTasks": [
{
"id": 213,
"name": "Task A"
}
]
}
],
"mentee": {
"name": "Mentee Joe",
"id": 212
}
}
Note that using the pattern comprehensions, you can avoid the OPTIONAL matches. If a pattern comprehension does not find anything, it returns []

toJson() invalid when recursively encode tree data structure

I have a Dart class that I am using as a node class for a tree data structure.
My goal here is to encode objects of this class and its child nodes recursively.
I have a toJson() method that takes the child Nodes List and calls jsonencode on them.
class Node{
String name;
Map<String, String> attributes;
List<Node> children = List<Node>();
Node(this.name, attributes) {
this.attributes = attributes;
this.children = List<Node>();
}
Node.fromJson(Map<dynamic,dynamic> _map) {
this.name = _map['name'];
this.children = new List<Node>();
this.attributes = _map['attributes'][0];
for(var i = 0; i < _map['children'].length;i++){
Node temp = new Node.fromJson(_map['children'][i]);
this.addChild(temp);
}
}
Map<String, dynamic> toJson() => {
'name': name,
'attributes': [attributes],
'children': [
...this.children.map(jsonEncode)
]
};
}
I have a unit test i created to test this functionality:
Node nodeMap = {
"name": "Name",
"attributes": [
{"#htag1": "tagval1"}
],
"children": [
{
"name": "NameChild1",
"attributes": [
{"#htag2": "tagval2"}
],
"children": []
},
{
"name": "NameChild2",
"attributes": [
{"#htag3": "tagval3"}
],
"children": []
}
]
};
UNode unodeInst = new UNode.fromJson(nodeMap);
// Act
var nodeCreate = nodeInst.toJson();
// Assert
expect(nodeCreate, equals(nodeMap));
Here is the output of my unit test
Expected: {
'name': 'Name',
'attributes': [{'#htag1': 'tagval1'}],
'children': [
{
'name': 'NameChild1',
'attributes': [{'#htag2': 'tagval2'}],
'children': []
},
{
'name': 'NameChild2',
'attributes': [{'#htag3': 'tagval3'}],
'children': []
}
]
}
Actual: {
'name': 'Name',
'attributes': [{'#htag1': 'tagval1'}],
'children': [
'{"name":"NameChild1","attributes":[{"#htag2":"tagval2"}],"children":[]}',
'{"name":"NameChild2","attributes":[{"#htag3":"tagval3"}],"children":[]}'
]
}
Which: at location ['children'][0] is '{"name":"NameChild1","attributes":[{"#htag2":"tagval2"}],"children":[]}' which expected a map
As you see its not encoding my object correctly.
I believe this is happening because when i reclusively call jsonencode this method returns a string that is placed into the children array.
I believe part of my problem is that i dont fully understand the d diffrence between jsonencode() and toJson().
It is my understanding that jsonencode() calls toJson().. but jsonencode() returns a string and toJson() returns a Map<String, dynamic>.. so i think what i want here is to call toJson() recursively and not jsonencode.
Does this sound correct?
But i cannot figure out how to do this on a list in this situation.
I have tried the following
...this.children.map(this.toJson())
but i get "The argument type 'Map<String, dynamic>' can't be assigned to the parameter type 'dynamic Function(Node)'"
...this.children.forEach((element) {element.toJson()})
but i get "Spread elements in list or set literals must implement 'Iterable'"
Does this mean i have to implement the Iterable interface in my class?
You're just using the map method incorrectly. Use the following instead.
[
...this.children.map((e) => e.toJson())
]
It's also unnecessary to use spread with a literal list or use this. You can simplify the code to just
children.map((e) => e.toJson()).toList()

How to use Promise.all in graphql resolver for neo4j?

I have multiple user nodes (around 45 users) and I want to return the total users count as well as the User's details too in a single query. (Similar to How to design the following resolver for GraphQL server?)
My schema:
type User {
ID: Int
name: String
}
type Query {
users: [User]
}
And after running the resolver for users query, I want to pull the total count as well as the users details too like below:
{
"data": {
"users": {
"total": 45
"users": [
{
"ID": 1,
"name": "User A"
},
{
"ID": 2,
"name": "User B"
},
...
]
}
But I am confused how to use Promise.all in neo4j. I tried to look how the promise works in neo4j but I did not find any desired info.
So, could you please let me know how should I write my resolver for this case ? Any help would be appreciable !!
Using Promise.all is not different in neo4j. Promise.all is from javascript. To write resolver you can do following:
let countQuery = "MATCH(n:User) RETURN count(n) as count;";
let userQuery = "MATCH(u:User) RETURN u;";
return Promise.all([
dbSession().run(countQuery, params),
dbSession().run(userQuery, params)
]).then((data) => {
return {
total: data[0].records.map(record => {return record.get('count')}
users: data[1].records.map(record => {return record.get('u')}
}
})
In your schema type you can change it to following:
type User {
ID: Int
name: String
}
type PagedData {
total: Int,
users: [User]
}
type Query {
users: PagedData
}

graphql-ruby: Int isn't a defined input type (on $first)

I’ve got a question I can’t seemingly resolve on my own.
Together with basic Query, Mutation and so on types I’ve made the following type definition:
module Types
UserType = GraphQL::ObjectType.define do
name 'User'
description 'A user'
implements GraphQL::Relay::Node.interface
global_id_field :id
field :email, !types.String, 'Email address'
connection :docs, DocType.connection_type, 'Available docs'
end
end
And I then try to query it with:
query FileListQuery(
$after: String
$first: Int
) {
viewer {
currentUser {
docs(first: $first, after: $after) {
edges {
node {
id
name
__typename
}
cursor
}
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
}
id
}
id
}
}
And I pass the following as query variables:
{
"first": 1,
"after": null
}
The problem is it bails out with the following:
{
"errors": [
{
"message": "Int isn't a defined input type (on $first)",
"locations": [
{
"line": 3,
"column": 3
}
],
"fields": [
"query FileListQuery"
]
}
]
}
I honestly have no clue why it complains about the Int type…
If I get rid of the problematic $first query variable in the request, it works fine.
This:
query FileListQuery(
$after: String
) {
viewer {
currentUser {
docs(first: 10, after: $after) {
edges {
node {
id
name
__typename
}
cursor
}
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
}
id
}
id
}
}
Produces this:
{
"data": {
"viewer": {
"currentUser": {
"docs": {
"edges": [
{
"node": {
"id": "1",
"name": "First Doc",
"__typename": "Doc"
},
"cursor": "MQ=="
}
],
"pageInfo": {
"endCursor": "MQ==",
"hasNextPage": false,
"hasPreviousPage": false,
"startCursor": "MQ=="
}
},
"id": "1"
},
"id": "VIEWER"
}
}
}
Any hints, ideas on how to fix this? I use the graphql gem v1.6.3.
Currently, there seems to be a bug in graphql-ruby that prevents types not explicitly used in a schema from being propagated. Check out this issue on GitHub: https://github.com/rmosolgo/graphql-ruby/issues/788#issuecomment-308996229
To fix the error one has to include an Int field somewhere in the schema. Turns out I haven't had one. Yikes.
This fixed it for me:
# Make sure Int is included in the schema:
field :testInt, types.Int

Resources