Open richburdon opened 7 years ago
Does ignoring the possibility of tasks belonging to multiple projects simplify anything? If not we should solve the more general many-to-many problem.
For posterity, I'll enumerate the 4 types of composition we discussed the other day:
The schema you wrote above represents the relationship directly (2), but the query you've sketched is more like (4).
For discussion here's an approach that builds on top of a relational model underneath (imagine the resolvers use sql tables underneath to represent the relationships between projects, tasks and users):
type Item {
...
links: [Item]
}
type Task implements Item {
assignee: User
owner: User
}
type Project implements Item {
// nothing special here... all relations will be links.
}
query projectQuery($itemId: ID!) {
item(itemId: $itemId) {
links {
... on Task {
assignee: { ... UserFragment }
owner: { ... UserFragment }
}
}
}
}
When the client wants to build a page that groups tasks by assignee, it has to sort them into user buckets at the client -- the query response is not structured that way. But the shape of the query is simple and general.
During resolution, first the server would fetch a project by ID and retrieve the item keys for all links, then as it went down the response tree each linked item would be resolved in turn.
Any other problems w/ this approach?
Let's give examples for the four cases:
I think Schema is very powerful for the following reasons (relative to a homogenous system):
I don't understand your SQL analogy. Typically SQL have type-specific tables and references. I don't think the different models really affect the backend implementation.
However...
Against schema:
I think you client side sorting "problem" isn't relevant: you can still do nesting in the query if that's useful to you. It won't be as clear to traverse (e.g., items.items.items.items) and I'm not sure what the traversal syntax would be for "members" vs "participants" things.
I think in summary the trade-off are: For Schema: clarity, constraints, simplicity, contextual matching (i.e., not expressed purely by filter). Against: Flexibility, possibility of client side holy grail caching (offline).
My comment about making resolvers automatic still would have issues. There is always going to be some opaqueness in the queries (context, ACL, the current concept of refs for nested queries, etc.) So for offline we will need to some extent a shallow implementation of the resolvers. I think of these as stored procedures.
Let's compare some real life queries side by side.
Agreed, assignee vs owner is a good canonical example of the need for some type-specific schema.
Agreed that using schema with nice field names to capture relationships makes deep nested queries easier to write, e.g. project.tasks.assignee.name instead of something with a bunch of filters.
For the record here's some issues that I believe motivate this discussion:
Some questions related to the links between Projects and People:
Worth noting two issues w a very generic approach that we identified when chatting:
Heterogeneous pagination, e.g. paginating separately through tasks vs notes attached to a project. But we can handle with multiple nested queries with different filters, something supported by graphql. e.g.:
item(itemid: $itemId) {
notes: links(type=Note, offset=...) { NoteFragment }
tasks: links(type=Task, offset=...) { TaskFragment }
}
The assignee vs owner problem (same as your "participant vs observer" example). Ie. differentiating by pure link semantics that are not captured anywhere else in the object data. The only generic solution that comes to mind is first-class Link objects with a type field -- like freebase subject, predicate, object triples.
My comment about SQL was really about where the relationship is expressed: In our current approach, the interesting relationships are represented by embedded queries (type 4 in our typology) that the client needs to express. E.g.
tasks(filter: { expr: { field: "project", value: $itemId } })
or
tasks(filter: { expr: { field: "assignee", ref: "id" } } AND project=this (paraphrasing)
that feels awkward to me -- the relationship between tasks and the parent project should already be known, not expressed again in the filter of an inner query that the client has to write.
Consider:
Event => Project(label=eng) => Task(status=blocking)
Query:
{
project(label: eng) {
tasks(status: blocking)
}
}
{
links(type:Project, label:eng) {
... on Project { ... }
links(type:Task, status:blocking) {
... on Task {
assignee: {
name
}
}
}
}
}
Issues:
type Project {
notes(PaginationFilter): [Note]
tasks(PaginationFilter): [Task]
projects(PaginationFilter): [Project]
documents(PaginationFilter): [Document]
}
vs
type Project {
links(FilterInput): [Item]
}
Or:
type Item {
links(relation, filter)
}
Consider the following Project->Task structure (ignoring for the moment the possibility of tasks belonging to multiple projects).
[OPTIMIZATION ISSUE: If we just want to retrieve the project ID, can we do this in the resolver without having to retrieve the Project record (we already have the ID value in the Task record -- should we proactively parse the query shape to determine if the actual record should be retrieved?) Is this moot once we have links?]
In the ProjectCard, we could do this:
And/or implement Project->Task hierarchies (aka Item "compositions") via parent->child references.
And filter these by member/assignee in the renderer.
At some point we'll also need to be able to reference links in the filter.