Package: datasette-graphql
datasette-graphqlDatasette plugin providing an automatic GraphQL API for your SQLite databasesRead more about this project:GraphQL in Datasette with the new datasette-graphql pluginTry out a live demo atdatasette-graphql-demo.datasette.io/graphqlInstallationConfigurationUsageQuerying for tables and columnsFetching a single recordAccessing nested objectsAccessing related objectsFiltering tablesSortingPaginationSearchColumns containing JSON stringsAuto camelCaseCORSExecution limitsThe graphql() template functionAdding custom fields with pluginsDevelopmentInstallationInstall this plugin in the same environment as Datasette.$ datasette install datasette-graphqlConfigurationBy default this plugin adds the GraphQL API at/graphql. You can configure a different path using thepathplugin setting, for example by adding this tometadata.json:{"plugins":{"datasette-graphql":{"path":"/-/graphql"}}}This will set the GraphQL API to live at/-/graphqlinstead.UsageThis plugin sets up/graphqlas a GraphQL endpoint for the first attached database.If you have multiple attached databases each will get its own endpoint at/graphql/name_of_database.The automatically generated GraphQL schema is available at/graphql/name_of_database.graphql- here'san example.Querying for tables and columnsIndividual tables (and SQL views) can be queried like this:{repos{nodes{idfull_namedescription_}}}Try this queryIn this example query the underlying database table is calledreposand its columns includeid,full_nameanddescription. Sincedescriptionis a reserved word the query needs to ask fordescription_instead.Fetching a single recordIf you only want to fetch a single record - for example if you want to fetch a row by its primary key - you can use thetablename_rowfield:{repos_row(id:107914493){idfull_namedescription_}}Try this queryThetablename_rowfield accepts the primary key column (or columns) as arguments. It also supports the samefilter:,search:,sort:andsort_desc:arguments as thetablenamefield, described below.Accessing nested objectsIf a column is a foreign key to another table, you can request columns from the table pointed to by that foreign key using a nested query like this:{repos{nodes{idfull_nameowner{idlogin}}}}Try this queryAccessing related objectsIf another table has a foreign key back to the table you are accessing, you can fetch rows from that related table.Consider auserstable which is related torepos- a repo has a foreign key back to the user that owns the repository. Theusersobject type will have arepos_by_owner_listfield which can be used to access those related repos:{users(first:1,search:"simonw"){nodes{namerepos_by_owner_list(first:5){totalCountnodes{full_name}}}}}Try this queryFiltering tablesYou can filter the rows returned for a specific table using thefilter:argument. This accepts a filter object mapping columns to operations. For example, to return just repositories with the Apache 2 license and more than 10 stars:{repos(filter:{license:{eq:"apache-2.0"},stargazers_count:{gt:10}}){nodes{full_namestargazers_countlicense{key}}}}Try this querySeetable filters examplesfor more operations, andcolumn filter argumentsin the Datasette documentation for details of how those operations work.These same filters can be used on nested relationships, like so:{users_row(id:9599){namerepos_by_owner_list(filter:{name:{startswith:"datasette-"}}){totalCountnodes{full_name}}}}Try this queryThewhere:argument can be used as an alternative tofilter:when the thing you are expressing is too complex to be modeled using a filter expression. It accepts a string fragment of SQL that will be included in theWHEREclause of the SQL query.{repos(where:"name='sqlite-utils' or name like 'datasette-%'"){totalCountnodes{full_name}}}Try this querySortingYou can set a sort order for results from a table using thesort:orsort_desc:arguments. The value for this argument should be the name of the column you wish to sort (or sort-descending) by.{repos(sort_desc:stargazers_count){nodes{full_namestargazers_count}}}Try this queryPaginationBy default the first 10 rows will be returned. You can control this using thefirst:argument.{repos(first:20){totalCountpageInfo{hasNextPageendCursor}nodes{full_namestargazers_countlicense{key}}}}Try this queryThetotalCountfield returns the total number of records that match the query.Requesting thepageInfo.endCursorfield provides you with the value you need to request the next page. You can pass this to theafter:argument to request the next page.{repos(first:20,after:"134874019"){totalCountpageInfo{hasNextPageendCursor}nodes{full_namestargazers_countlicense{key}}}}Try this queryThehasNextPagefield tells you if there are any more records.SearchIf a table has been configured to use SQLite full-text search you can execute searches against it using thesearch:argument:{repos(search:"datasette"){totalCountpageInfo{hasNextPageendCursor}nodes{full_namedescription_}}}Try this queryThesqlite-utilsPython library and CLI tool can be used to add full-text search to an existing database table.Columns containing JSON stringsIf your table has a column that contains data encoded as JSON,datasette-graphqlwill make that column available as an encoded JSON string. Clients calling your API will need to parse the string as JSON in order to access the data.You can return the data as a nested structure by configuring that column to be treated as a JSON column. Theplugin configurationfor that inmetadata.jsonlooks like this:{"databases":{"test":{"tables":{"repos":{"plugins":{"datasette-graphql":{"json_columns":["tags"]}}}}}}}Auto camelCaseThe names of your columns and tables default to being matched by their representations in GraphQL.If you have tables withnames_like_thisyou may want to work with them in GraphQL usingnamesLikeThis, for consistency with GraphQL and JavaScript conventions.You can turn on automatic camelCase using the"auto_camelcase"plugin configuration setting inmetadata.json, like this:{"plugins":{"datasette-graphql":{"auto_camelcase":true}}}CORSThis plugin obeys the--corsoption passed to thedatasettecommand-line tool. If you pass--corsit adds the following CORS HTTP headers to allow JavaScript running on other domains to access the GraphQL API:access-control-allow-headers: content-type
access-control-allow-method: POST
access-control-allow-origin: *Execution limitsThe plugin implements two limits by default:The total time spent executing all of the underlying SQL queries that make up the GraphQL execution must not exceed 1000ms (one second)The total number of SQL table queries executed as a result of nested GraphQL fields must not exceed 100These limits can be customized using thenum_queries_limitandtime_limit_msplugin configuration settings, for example inmetadata.json:{"plugins":{"datasette-graphql":{"num_queries_limit":200,"time_limit_ms":5000}}}Setting these to0will disable the limit checks entirely.The graphql() template functionThe plugin also makes a Jinja template function available calledgraphql(). You can use that function in your Datasettecustom templateslike so:{%setusers=graphql("""{users {nodes {namepointsscore}}}""")["users"]%}{%foruserinusers.nodes%}<p>{{user.name}}- points:{{user.points}}, score ={{user.score}}</p>{%endfor%}The function executes a GraphQL query against the generated schema and returns the results. You can assign those results to a variable in your template and then loop through and display them.By default the query will be run against the first attached database. You can use the optional second argument to the function to specify a different database - for example, to run against an attachedgithub.dbdatabase you would do this:{%setuser=graphql("""{users_row(id:9599) {nameloginavatar_url}}""","github")["users_row"]%}<h1>Hello,{{user.name}}</h1>You can useGraphQL variablesin these template calls by passing them to thevariables=argument:{%setuser=graphql("""query ($id: Int) {users_row(id: $id) {nameloginavatar_url}}""",database="github",variables={"id":9599})["users_row"]%}<h1>Hello,{{user.name}}</h1>Adding custom fields with pluginsdatasette-graphqladds a newplugin hookto Datasette which can be used to add custom fields to your GraphQL schema.The plugin hook looks like this:@hookimpldefgraphql_extra_fields(datasette,database):"A list of (name, field_type) tuples to include in the GraphQL schema"You can use this hook to return a list of tuples describing additional fields that should be exposed in your schema. Each tuple should consist of a string naming the new field, plus aGraphene Field objectthat specifies the schema and provides aresolverfunction.This example implementation usespkg_resourcesto return a list of currently installed Python packages:importgraphenefromdatasetteimporthookimplimportpkg_resources@hookimpldefgraphql_extra_fields():classPackage(graphene.ObjectType):"An installed package"name=graphene.String()version=graphene.String()defresolve_packages(root,info):return[{"name":d.project_name,"version":d.version}fordinpkg_resources.working_set]return[("packages",graphene.Field(graphene.List(Package),description="List of installed packages",resolver=resolve_packages,),),]With this plugin installed, the following GraphQL query can be used to retrieve a list of installed packages:{packages{nameversion}}DevelopmentTo set up this plugin locally, first checkout the code. Then create a new virtual environment:cd datasette-graphql
python3 -mvenv venv
source venv/bin/activateOr if you are usingpipenv:pipenv shellNow install the dependencies and tests:pip install -e '.[test]'To run the tests:pytest