-
-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Forward pagination with nodes, edges, totalCount and pageInfo #2
Comments
It looks like this is the community standard to follow: https://relay.dev/graphql/connections.htm |
I'm going to start by imitating the GitHub API, but implementing only As far as I can tell I don't need that yet, and it looks like I can implement Here's a sample GitHub GraphQL API query: https://developer.github.com/v4/explorer/ query {
viewer {
repositories(first:5) {
__typename
nodes {
name
}
totalCount
pageInfo {
endCursor
startCursor
}
}
}
} Which produces this output: {
"data": {
"viewer": {
"repositories": {
"__typename": "RepositoryConnection",
"nodes": [
{
"name": "django-debug-toolbar"
},
{
"name": "simonw.github.com"
},
{
"name": "ratelimitcache"
},
{
"name": "api-explorer"
},
{
"name": "python-guardianapi"
}
],
"totalCount": 385,
"pageInfo": {
"endCursor": "Y3Vyc29yOnYyOpHOAAIfBA==",
"startCursor": "Y3Vyc29yOnYyOpHN1ZQ="
}
}
}
}
} Here's a query that also shows their support for {
viewer {
repositories(first: 5, after: "Y3Vyc29yOnYyOpHOAAGSAg==") {
__typename
nodes {
id
name
}
edges {
cursor
node {
id
name
}
}
totalCount
pageInfo {
endCursor
startCursor
hasNextPage
hasPreviousPage
}
}
}
} |
I think I can only do Urgh cursors will be hard because that cursor logic is embedded deep in the tangle of the Datasette |
Maybe I do need to dispatch these calls to the TableView class. |
I'm going to try that. I can create a fake def fake(cls, path_with_query_string, method="GET", scheme="http"): Then I can create a async def data(
self,
request,
database,
hash,
table,
default_labels=False,
_next=None,
_size=None,
): That method ends with this return statement - which includes the stuff that I need: return (
{
"database": database,
"table": table,
"is_view": is_view,
"human_description_en": human_description_en,
"rows": rows[:page_size],
"truncated": results.truncated,
"filtered_table_rows_count": filtered_table_rows_count,
"expanded_columns": expanded_columns,
"expandable_columns": expandable_columns,
"columns": columns,
"primary_keys": pks,
"units": units,
"query": {"sql": sql, "params": params},
"facet_results": facet_results,
"suggested_facets": suggested_facets,
"next": next_value and str(next_value) or None,
"next_url": next_url,
"private": private,
"allow_execute_sql": await self.ds.permission_allowed(
request.actor, "execute-sql", database, default=True
),
},
extra_template,
(
"table-{}-{}.html".format(to_css_class(database), to_css_class(table)),
"table.html",
),
) Using Datasette internals like this is a bit risky, since future changes to Datasette could easily break this plugin. This should encourage me to finally refactor the TableView to be cleaner and easier to use from other plugins though. |
It looks like the Relay specification really wants you to use I'm going to stick with my Adapted from the example on https://relay.dev/graphql/connections.htm I'm going to go with: friends(first: 10, after: "opaqueCursor") {
nodes {
id
name
}
pageInfo {
hasNextPage
cursor
}
} The current Relay PageInfo spec asks for the following fields:
The spec says:
I'm going to just return |
https://jeffersonheard.github.io/python/graphql/2018/12/08/graphene-python.html is really useful. Helped me understand that even without additional properties the "edge" concept is useful because you can have a cursor on each edge - at which point the startCursor and endCursor are just the first and last of those values for the current page. |
https://github.com/graphql-python/graphene-sqlalchemy/blob/master/examples/flask_sqlalchemy/app.py is an example app using It all comes down to subclasses of |
I'm going to support both |
This may be the kick I need to get Datasette pagination to work in reverse too. |
For the non-alpha release I'm going to support forwards pagination only - I'll split |
How easy is it for me to generate cursors for individual records? In Datasette core the |
Here's the Datasette core logic that figures out the if is_view:
next_value = int(_next or 0) + page_size
else:
next_value = path_from_row_pks(rows[-2], pks, use_rowid)
# If there's a sort or sort_desc, add that value as a prefix
if (sort or sort_desc) and not is_view:
prefix = rows[-2][sort or sort_desc]
if isinstance(prefix, dict) and "value" in prefix:
prefix = prefix["value"]
if prefix is None:
prefix = "$null"
else:
prefix = urllib.parse.quote_plus(str(prefix))
next_value = "{},{}".format(prefix, next_value) |
|
As a reminder, here's a pagination example from regular Datasette: Here' the token is |
Datasette encodes next tokens using URL encoding. The GraphQL standard seems to be base64 instead, so I'll use that here. |
I'm going to set the default page size to 100 (if no |
I'm going to copy code over that I need from I may well extract duplicated code into a documented API (or a separate package entirely, maybe |
This looks like the most relevant code from _boring_keyword_re = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
def escape_sqlite(s):
if _boring_keyword_re.match(s) and (s.lower() not in reserved_words):
return s
else:
return "[{}]".format(s)
def urlsafe_components(token):
"Splits token on commas and URL decodes each component"
return [urllib.parse.unquote_plus(b) for b in token.split(",")]
def path_from_row_pks(row, pks, use_rowid, quote=True):
""" Generate an optionally URL-quoted unique identifier
for a row from its primary keys."""
if use_rowid:
bits = [row["rowid"]]
else:
bits = [
row[pk]["value"] if isinstance(row[pk], dict) else row[pk] for pk in pks
]
if quote:
bits = [urllib.parse.quote_plus(str(bit)) for bit in bits]
else:
bits = [str(bit) for bit in bits]
return ",".join(bits)
def compound_keys_after_sql(pks, start_index=0):
# Implementation of keyset pagination
# See https://github.com/simonw/datasette/issues/190
# For pk1/pk2/pk3 returns:
#
# ([pk1] > :p0)
# or
# ([pk1] = :p0 and [pk2] > :p1)
# or
# ([pk1] = :p0 and [pk2] = :p1 and [pk3] > :p2)
or_clauses = []
pks_left = pks[:]
while pks_left:
and_clauses = []
last = pks_left[-1]
rest = pks_left[:-1]
and_clauses = [
"{} = :p{}".format(escape_sqlite(pk), (i + start_index))
for i, pk in enumerate(rest)
]
and_clauses.append(
"{} > :p{}".format(escape_sqlite(last), (len(rest) + start_index))
)
or_clauses.append("({})".format(" and ".join(and_clauses)))
pks_left.pop()
or_clauses.reverse()
return "({})".format("\n or\n".join(or_clauses)) |
This was really helpful in reminding me how to do this: simonw/datasette#190 |
On closer reading of https://relay.dev/graphql/connections.htm#sec-undefined.PageInfo.Fields it looks like There's some further context from GraphQL co-creator Dan Schafer in this comment: graphql/graphql-relay-js#58 (comment)
It looks like they went ahead and made that change: https://relay.dev/graphql/connections.htm#sec-undefined.PageInfo.Fields specifically says about
So I only need it to actually work when I implement |
Writing the code for this is tricky. I'm going to build a one-off implementation for just a single table first, then figure out how to generalize it to every table. |
This query now returns the correct-ish results: { repos(filters: [stargazers__gt=1]) { totalCount nodes { id full_name } edges { cursor node { id full_name } } } }
It looks to me like most of the work will happen in the resolve method for the table: datasette-graphql/datasette_graphql/utils.py Lines 171 to 191 in ff48a6f
With a little bit of extra code on the class for that table - but really just to pull out the right bits into the edges / nodes / pageInfo fields: datasette-graphql/datasette_graphql/utils.py Lines 60 to 83 in ff48a6f
As such, I think the That way if you ask for both |
This mechanism will also handle the "ask for page size + 1 so we can tell if there's a next page" logic. |
I'm going to have a quick go at reusing the I could have the |
Needs documentation, then I can merge it. |
Also fixed first link to live demo. Refs #2
https://graphql.org/learn/pagination/
The text was updated successfully, but these errors were encountered: