grist-core
grist-core copied to clipboard
Enhancement: GraphQL endpoint
As a follow-up to #757, it would be very handy, for custom plugins, to have a GraphQL endpoint. This would allow, in only one query, to get all information needed, no more. But I'm not sure I realize how much work it would be, especially to deal with access rules!
I don’t know whether it would be useful, but https://github.com/bradleyboy/tuql seems able to automatically build such a service from a SQLite database as soon as foreign keys are defined within the database.
This will also be useful to include Grist databases into GraphQL-federation-backed data meshes.
After a quick test with an adapted OpenAPI spec and little manual modifications, it is possible to hook Grist behind a GraphQL API with using Hasura Actions.
Taking https://github.com/gristlabs/grist-help/blob/master/api/grist.yml and changing it like:
13c13
< - url: https://{subdomain}.getgrist.com/api
---
> - url: https://data.example.com/api
1283,1299c1283
< WebhookProperties:
< size:
< type: number
< example: 1
< attempts:
< type: number
< example: 1
< errorMessage:
< type: string
< nullable: true
< example: null
< httpStatus:
< type: number
< example: 200
< status:
< type: string
< example: "success"
---
> WebhookProperties: {}
1747c1731
< enum: [Any, Text, Numeric, Int, Bool, Date, DateTime:<timezone>, Choice, ChoiceList, Ref:<tableId>, RefList:<tableId>, Attachments]
---
> enum: [Any, Text, Numeric, Int, Bool, Date, "DateTime:<timezone>", Choice, ChoiceList, "Ref:<tableId>", "RefList:<tableId>", Attachments]
2006,2009d1989
< dialect:
< $ref: https://specs.frictionlessdata.io/schemas/csv-dialect.json
< schema:
< $ref: https://specs.frictionlessdata.io/schemas/table-schema.json
The additions and removals were necessary, due to the schema failing validation in the Hasura Console.
After reading up a bit on nullability in GraphQL, it became necessary to remove the ! behind the User type of the owner property in the Org type at http://127.127.0.3:8080/console/actions/manage/listOrgs/modify.
Some generated queries already include certain headers (X-Sort, X-Limit), which must be deleted, due to malformed template values like {{$body.input?.X-Sort}} and {{$body.input?.X-Limit}}, which somehow don't work.
After adding the Authorization token to the environment and modifying the listOrg action to contain an Authorization header from the GRIST_TOKEN environmental variable, it became possible to query Grist with GraphQL in the Hasura Console. It could be interesting to look into automating this procedure. Hasura has a nice export functionality, which produces a declarative description of the system.
Implementation
Hasura and Docker Compose .env.example:
DATA_POSTGRES_USER=localhost-example-data
DATA_POSTGRES_PASSWORD=
META_POSTGRES_USER=localhost-example-meta
META_POSTGRES_PASSWORD=
## postgres database to store Hasura metadata
# Database URL postgresql://username:password@hostname:5432/database
HASURA_GRAPHQL_METADATA_DATABASE_URL=postgresql://${META_POSTGRES_USER}:${META_POSTGRES_PASSWORD}@hasura-meta:5432/${META_POSTGRES_USER}
## this env var can be used to add the above postgres database to Hasura as a data source. this can be removed/updated based on your needs
PG_DATABASE_URL=postgresql://${DATA_POSTGRES_USER}:${DATA_POSTGRES_PASSWORD}@data:5432/${DATA_POSTGRES_USER}
## enable the console served by server
HASURA_GRAPHQL_ENABLE_CONSOLE=true # set to "false" to disable console
## enable debugging mode. It is recommended to disable this in production
HASURA_GRAPHQL_DEV_MODE=true
HASURA_GRAPHQL_ENABLED_LOG_TYPES=startup, http-log, webhook-log, websocket-log, query-log
## uncomment next line to run console offline (i.e load console assets from server instead of CDN)
# HASURA_GRAPHQL_CONSOLE_ASSETS_DIR=/srv/console-assets
## uncomment next line to set an admin secret
# HASURA_GRAPHQL_ADMIN_SECRET=myadminsecretkey
# For Hasura Actions to pick up their secret
GRIST_TOKEN=Bearer <token>
# For the Hasura CLI to talk to the correct endpoint
HASURA_GRAPHQL_ENDPOINT=http://127.127.0.3:8080
Docker Compose compose.yml:
networks:
internal:
x-default: &default
restart: unless-stopped
networks: ["internal"]
x-pg-default: &pg-default
<<: *default
image: postgres:15-alpine
healthcheck:
interval: 10s
retries: 10
test: "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\""
timeout: 2s
services:
data:
<<: *pg-default
ports:
- "127.127.0.1:5432:5432"
volumes:
- ./.state/postgres/data:/var/lib/postgresql/data
environment:
POSTGRES_USER: ${DATA_POSTGRES_USER}
POSTGRES_DB: ${DATA_POSTGRES_USER}
POSTGRES_PASSWORD: ${DATA_POSTGRES_PASSWORD}
hasura-meta:
<<: *pg-default
ports:
- "127.127.0.2:5432:5432"
volumes:
- ./.state/postgres/meta:/var/lib/postgresql/data
environment:
POSTGRES_USER: ${META_POSTGRES_USER}
POSTGRES_DB: ${META_POSTGRES_USER}
POSTGRES_PASSWORD: ${META_POSTGRES_PASSWORD}
hasura-console:
<<: *default
image: hasura/graphql-engine
ports:
- "127.127.0.3:8080:8080"
depends_on:
hasura-meta:
condition: service_healthy
data:
condition: service_healthy
environment:
- PG_DATABASE_URL
- HASURA_GRAPHQL_METADATA_DATABASE_URL
- HASURA_GRAPHQL_ENABLE_CONSOLE
- HASURA_GRAPHQL_DEV_MODE
- HASURA_GRAPHQL_ENABLED_LOG_TYPES
- GRIST_TOKEN
justfile:
install:
if [ ! -f .env ]; then \
cp .env.example .env; \
perl -pe "s/(?<=DATA_POSTGRES_PASSWORD=).*/$(openssl rand -hex 32)/" -i .env; \
perl -pe "s/(?<=META_POSTGRES_PASSWORD=).*/$(openssl rand -hex 32)/" -i .env; \
sed "s/<token>/${GRIST_TOKEN}/" -i .env; \
fi
docker compose pull
run:
docker compose up -d
import:
hasura metadata apply
export:
hasura metadata export
With the above files and the Just task action runner, you should be able to run:
GRIST_TOKEN=<generate on https://data.example.com/o/docs/account> just install
just run
It would be possible to adapt the justfile into a regular shell script, but that is often not what's needed.
Then you can switch to http://127.127.0.3:8080/console/ and configure your OpenAPI import and update certain types in the Actions, or use the hasura CLI, which will read its endpoint from the .env file.
Loading OpenAPI-generated and manually modified Hasura Action definitions
The hasura metadata commands provide convenient access to the definitions of the generated actions, which can be easily modified.
just export runs hasura metadata export and generates a metadata/ directory, in which two files are not empty.
just import runs hasura metadata import to read the files from the expected places and applies them to the Hasura environment.
An example Hasura working directory will follow the conventions of the directories when generated with hasura init.
Alternatively, the Hasura endpoint can also be configured in the generated config.yaml, which in this case might read:
version: 3
endpoint: http://127.127.0.3:8080
metadata_directory: metadata
actions:
kind: synchronous
handler_webhook_baseurl: http://127.127.0.3:8080
There are also other convenience methods, like hasura metadata diff, which is useful for previewing the changes during an apply run, in case something was modified locally in the files.
actions.graphql:
type Mutation {
addColumns(
createColumnsInput: CreateColumnsInput!
docId: String!
tableId: String!
): ColumnsWithoutFields
}
type Mutation {
addRecords(
docId: String!
noparse: Boolean
recordsWithoutIdInput: RecordsWithoutIdInput!
tableId: String!
): RecordsWithoutFields
}
type Mutation {
addRows(
dataWithoutIdInput: JSON!
docId: String!
noparse: Boolean
tableId: String!
): [Int]
}
type Mutation {
addTables(
createTablesInput: CreateTablesInput!
docId: String!
): TablesWithoutFields
}
type Mutation {
createDoc(
docParametersInput: DocParametersInput!
workspaceId: Int!
): String
}
type Mutation {
createWorkspace(
orgId: JSON!
workspaceParametersInput: WorkspaceParametersInput!
): Int
}
type Mutation {
deleteColumn(
colId: String!
docId: String!
tableId: String!
): String
}
type Mutation {
deleteDoc(
docId: String!
): String
}
type Mutation {
deleteRows(
docId: String!
rowIdsInput: [Int]!
tableId: String!
): String
}
type Mutation {
deleteWorkspace(
workspaceId: Int!
): String
}
type Query {
describeDoc(
docId: String!
): DocWithWorkspace
}
type Query {
describeOrg(
orgId: JSON!
): Org
}
type Query {
describeWorkspace(
workspaceId: Int!
): WorkspaceWithDocsAndOrg
}
type Query {
getTableData(
docId: String!
filter: String
limit: Float
sort: String
tableId: String!
xLimit: Float
xSort: String
): JSON
}
type Query {
listColumns(
docId: String!
hidden: Boolean
tableId: String!
): ColumnsList
}
type Query {
listDocAccess(
docId: String!
): DocAccessRead
}
type Query {
listOrgAccess(
orgId: JSON!
): OrgAccessRead
}
type Query {
listOrgs: [Org]
}
type Query {
listRecords(
docId: String!
filter: String
hidden: Boolean
limit: Float
sort: String
tableId: String!
xLimit: Float
xSort: String
): RecordsList
}
type Query {
listTables(
docId: String!
): TablesList
}
type Query {
listWorkspaceAccess(
workspaceId: Int!
): WorkspaceAccessRead
}
type Query {
listWorkspaces(
orgId: JSON!
): [WorkspaceWithDocsAndDomain]
}
type Mutation {
modifyColumns(
docId: String!
tableId: String!
updateColumnsInput: UpdateColumnsInput!
): String
}
type Mutation {
modifyDoc(
docId: String!
docParametersInput: DocParametersInput!
): String
}
type Mutation {
modifyDocAccess(
docAccessInput: DocAccessInput!
docId: String!
): String
}
type Mutation {
modifyOrg(
orgId: JSON!
orgParametersInput: OrgParametersInput!
): String
}
type Mutation {
modifyOrgAccess(
orgAccessInput: OrgAccessInput!
orgId: JSON!
): String
}
type Mutation {
modifyRecords(
docId: String!
noparse: Boolean
recordsListInput: RecordsListInput!
tableId: String!
): String
}
type Mutation {
modifyRows(
dataInput: JSON!
docId: String!
noparse: Boolean
tableId: String!
): [Int]
}
type Mutation {
modifyTables(
docId: String!
tablesListInput: TablesListInput!
): String
}
type Mutation {
modifyWorkspace(
workspaceId: Int!
workspaceParametersInput: WorkspaceParametersInput!
): String
}
type Mutation {
modifyWorkspaceAccess(
workspaceAccessInput: WorkspaceAccessInput!
workspaceId: Int!
): String
}
type Mutation {
moveDoc(
docId: String!
docMoveInput: DocMoveInput
): String
}
type Mutation {
replaceColumns(
docId: String!
noadd: Boolean
noupdate: Boolean
replaceall: Boolean
tableId: String!
updateColumnsInput: UpdateColumnsInput!
): String
}
type Mutation {
replaceRecords(
allowEmptyRequire: Boolean
docId: String!
noadd: Boolean
noparse: Boolean
noupdate: Boolean
onmany: Onmany
recordsWithRequireInput: RecordsWithRequireInput!
tableId: String!
): String
}
enum Access {
owners
editors
viewers
}
enum Onmany {
first
none
all
}
enum Type {
Any
Text
Numeric
Int
Bool
Date
DateTimetimezone
Choice
ChoiceList
ReftableId
RefListtableId
Attachments
}
input DocParametersInput {
isPinned: Boolean
name: String
}
input DocMoveInput {
workspace: Int!
}
input OrgParametersInput {
name: String
}
input OrgAccessInput {
delta: OrgAccessWriteInput!
}
input OrgAccessWriteInput {
users: JSON!
}
input WorkspaceParametersInput {
name: String
}
input WorkspaceAccessInput {
delta: WorkspaceAccessWriteInput!
}
input WorkspaceAccessWriteInput {
maxInheritedRole: Access
users: JSON
}
input DocAccessInput {
delta: DocAccessWriteInput!
}
input DocAccessWriteInput {
maxInheritedRole: Access
users: JSON
}
input Records3ListItemInput {
fields: JSON!
}
input RecordsWithoutIdInput {
records: [Records3ListItemInput]!
}
input Records2ListItemInput {
fields: JSON!
id: Float!
}
input RecordsListInput {
records: [Records2ListItemInput]!
}
input Records5ListItemInput {
fields: JSON
require: JSON!
}
input RecordsWithRequireInput {
records: [Records5ListItemInput]!
}
input ColumnsListItemInput {
fields: JSON
id: String
}
input CreateTablesInput {
tables: [Tables2ListItemInput]!
}
input Tables2ListItemInput {
columns: [ColumnsListItemInput]!
id: String
}
input TablesListInput {
tables: [TablesListItemInput]!
}
input TablesListItemInput {
fields: JSON!
id: String!
}
input Columns3ListItemInput {
fields: CreateFieldsInput
id: String
}
input CreateColumnsInput {
columns: [Columns3ListItemInput]!
}
input CreateFieldsInput {
formula: String
isFormula: Boolean
label: String
recalcDeps: String
recalcWhen: Int
type: Type
untieColIdFromLabel: Boolean
visibleCol: Int
widgetOptions: String
}
input Columns5ListItemInput {
fields: Fields4Input!
id: String!
}
input Fields4Input {
colId: String
formula: String
isFormula: Boolean
label: String
recalcDeps: String
recalcWhen: Int
type: Type
untieColIdFromLabel: Boolean
visibleCol: Int
widgetOptions: String
}
input UpdateColumnsInput {
columns: [Columns5ListItemInput]!
}
type Org {
access: Access!
createdAt: String!
domain: String!
id: BigInt!
name: String!
owner: User
updatedAt: String!
}
type User {
id: BigInt!
name: String!
picture: String!
}
type DocWithWorkspace {
access: Access!
id: String!
isPinned: Boolean!
name: String!
urlId: String!
workspace: WorkspaceWithOrg!
}
type WorkspaceWithOrg {
access: Access!
id: BigInt!
name: String!
org: Org!
}
type OrgAccessRead {
users: [UsersListItem]!
}
type UsersListItem {
access: Access
email: String
id: Int!
name: String!
}
type Doc {
access: Access!
id: String!
isPinned: Boolean!
name: String!
urlId: String!
}
type WorkspaceWithDocsAndDomain {
access: Access!
docs: [Doc]!
id: BigInt!
name: String!
orgDomain: String
}
type WorkspaceWithDocsAndOrg {
access: Access!
docs: [Doc]!
id: BigInt!
name: String!
org: Org!
}
type Users2ListItem {
access: Access
email: String
id: Int!
name: String!
parentAccess: Access
}
type WorkspaceAccessRead {
maxInheritedRole: Access!
users: [Users2ListItem]!
}
type DocAccessRead {
maxInheritedRole: Access!
users: [Users2ListItem]!
}
type Records2ListItem {
fields: JSON!
id: Float!
}
type RecordsList {
records: [Records2ListItem]!
}
type Records4ListItem {
id: Float!
}
type RecordsWithoutFields {
records: [Records4ListItem]!
}
type TablesList {
tables: [TablesListItem]!
}
type TablesListItem {
fields: JSON!
id: String!
}
type Tables3ListItem {
id: String!
}
type TablesWithoutFields {
tables: [Tables3ListItem]!
}
type Columns2ListItem {
fields: GetFields
id: String
}
type ColumnsList {
columns: [Columns2ListItem]
}
type GetFields {
colRef: Int
formula: String
isFormula: Boolean
label: String
recalcDeps: [Int]
recalcWhen: Int
type: Type
untieColIdFromLabel: Boolean
visibleCol: Int
widgetOptions: String
}
type Columns4ListItem {
id: String!
}
type ColumnsWithoutFields {
columns: [Columns4ListItem]!
}
scalar BigInt
scalar JSON
actions.yaml:
actions:
- name: addColumns
definition:
kind: synchronous
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
body:
action: transform
template: '{{$body.input.createColumnsInput}}'
method: POST
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/docs/{{$body.input.docId}}/tables/{{$body.input.tableId}}/columns'
version: 2
- name: addRecords
definition:
kind: synchronous
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
body:
action: transform
template: '{{$body.input.recordsWithoutIdInput}}'
method: POST
query_params:
noparse: '{{$body.input?.noparse}}'
template_engine: Kriti
url: '{{$base_url}}/docs/{{$body.input.docId}}/tables/{{$body.input.tableId}}/records'
version: 2
- name: addRows
definition:
kind: synchronous
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
method: POST
query_params:
noparse: '{{$body.input?.noparse}}'
template_engine: Kriti
url: '{{$base_url}}/docs/{{$body.input.docId}}/tables/{{$body.input.tableId}}/data'
version: 2
comment: Deprecated in favor of `records` endpoints. We have no immediate plans to remove these endpoints, but consider `records` a better starting point for new projects.
- name: addTables
definition:
kind: synchronous
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
body:
action: transform
template: '{{$body.input.createTablesInput}}'
method: POST
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/docs/{{$body.input.docId}}/tables'
version: 2
- name: createDoc
definition:
kind: synchronous
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
body:
action: transform
template: '{{$body.input.docParametersInput}}'
method: POST
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/workspaces/{{$body.input.workspaceId}}/docs'
version: 2
response_transform:
body:
action: transform
template: '{{$body}}'
template_engine: Kriti
version: 2
- name: createWorkspace
definition:
kind: synchronous
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
body:
action: transform
template: '{{$body.input.workspaceParametersInput}}'
method: POST
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/orgs/{{$body.input.orgId}}/workspaces'
version: 2
- name: deleteColumn
definition:
kind: synchronous
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
method: DELETE
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/docs/{{$body.input.docId}}/tables/{{$body.input.tableId}}/columns/{{$body.input.colId}}'
version: 2
response_transform:
body:
action: transform
template: '{{$body}}'
template_engine: Kriti
version: 2
- name: deleteDoc
definition:
kind: synchronous
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
method: DELETE
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/docs/{{$body.input.docId}}'
version: 2
response_transform:
body:
action: transform
template: '{{$body}}'
template_engine: Kriti
version: 2
- name: deleteRows
definition:
kind: synchronous
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
method: POST
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/docs/{{$body.input.docId}}/tables/{{$body.input.tableId}}/data/delete'
version: 2
response_transform:
body:
action: transform
template: '{{$body}}'
template_engine: Kriti
version: 2
- name: deleteWorkspace
definition:
kind: synchronous
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
method: DELETE
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/workspaces/{{$body.input.workspaceId}}'
version: 2
response_transform:
body:
action: transform
template: '{{$body}}'
template_engine: Kriti
version: 2
- name: describeDoc
definition:
kind: ""
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
method: GET
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/docs/{{$body.input.docId}}'
version: 2
- name: describeOrg
definition:
kind: ""
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
method: GET
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/orgs/{{$body.input.orgId}}'
version: 2
- name: describeWorkspace
definition:
kind: ""
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
method: GET
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/workspaces/{{$body.input.workspaceId}}'
version: 2
- name: getTableData
definition:
kind: ""
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
method: GET
query_params:
filter: '{{$body.input?.filter}}'
limit: '{{$body.input?.limit}}'
sort: '{{$body.input?.sort}}'
template_engine: Kriti
url: '{{$base_url}}/docs/{{$body.input.docId}}/tables/{{$body.input.tableId}}/data'
version: 2
comment: Deprecated in favor of `records` endpoints. We have no immediate plans to remove these endpoints, but consider `records` a better starting point for new projects.
- name: listColumns
definition:
kind: ""
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
method: GET
query_params:
hidden: '{{$body.input?.hidden}}'
template_engine: Kriti
url: '{{$base_url}}/docs/{{$body.input.docId}}/tables/{{$body.input.tableId}}/columns'
version: 2
- name: listDocAccess
definition:
kind: ""
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
method: GET
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/docs/{{$body.input.docId}}/access'
version: 2
- name: listOrgAccess
definition:
kind: ""
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
method: GET
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/orgs/{{$body.input.orgId}}/access'
version: 2
- name: listOrgs
definition:
kind: ""
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
method: GET
query_params: {}
request_headers:
add_headers: {}
remove_headers:
- content-type
template_engine: Kriti
url: '{{$base_url}}/orgs'
version: 2
comment: This enumerates all the team sites or personal areas available.
- name: listRecords
definition:
kind: ""
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
method: GET
query_params:
filter: '{{$body.input?.filter}}'
hidden: '{{$body.input?.hidden}}'
limit: '{{$body.input?.limit}}'
sort: '{{$body.input?.sort}}'
request_headers:
add_headers: {}
remove_headers:
- content-type
template_engine: Kriti
url: '{{$base_url}}/docs/{{$body.input.docId}}/tables/{{$body.input.tableId}}/records'
version: 2
- name: listTables
definition:
kind: ""
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
method: GET
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/docs/{{$body.input.docId}}/tables'
version: 2
- name: listWorkspaceAccess
definition:
kind: ""
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
method: GET
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/workspaces/{{$body.input.workspaceId}}/access'
version: 2
- name: listWorkspaces
definition:
kind: ""
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
method: GET
query_params: {}
request_headers:
add_headers: {}
remove_headers:
- content-type
template_engine: Kriti
url: '{{$base_url}}/orgs/{{$body.input.orgId}}/workspaces'
version: 2
- name: modifyColumns
definition:
kind: synchronous
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
body:
action: transform
template: '{{$body.input.updateColumnsInput}}'
method: PATCH
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/docs/{{$body.input.docId}}/tables/{{$body.input.tableId}}/columns'
version: 2
response_transform:
body:
action: transform
template: '{{$body}}'
template_engine: Kriti
version: 2
- name: modifyDoc
definition:
kind: synchronous
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
body:
action: transform
template: '{{$body.input.docParametersInput}}'
method: PATCH
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/docs/{{$body.input.docId}}'
version: 2
response_transform:
body:
action: transform
template: '{{$body}}'
template_engine: Kriti
version: 2
- name: modifyDocAccess
definition:
kind: synchronous
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
body:
action: transform
template: '{{$body.input.docAccessInput}}'
method: PATCH
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/docs/{{$body.input.docId}}/access'
version: 2
response_transform:
body:
action: transform
template: '{{$body}}'
template_engine: Kriti
version: 2
- name: modifyOrg
definition:
kind: synchronous
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
body:
action: transform
template: '{{$body.input.orgParametersInput}}'
method: PATCH
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/orgs/{{$body.input.orgId}}'
version: 2
response_transform:
body:
action: transform
template: '{{$body}}'
template_engine: Kriti
version: 2
- name: modifyOrgAccess
definition:
kind: synchronous
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
body:
action: transform
template: '{{$body.input.orgAccessInput}}'
method: PATCH
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/orgs/{{$body.input.orgId}}/access'
version: 2
response_transform:
body:
action: transform
template: '{{$body}}'
template_engine: Kriti
version: 2
- name: modifyRecords
definition:
kind: synchronous
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
body:
action: transform
template: '{{$body.input.recordsListInput}}'
method: PATCH
query_params:
noparse: '{{$body.input?.noparse}}'
template_engine: Kriti
url: '{{$base_url}}/docs/{{$body.input.docId}}/tables/{{$body.input.tableId}}/records'
version: 2
response_transform:
body:
action: transform
template: '{{$body}}'
template_engine: Kriti
version: 2
- name: modifyRows
definition:
kind: synchronous
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
method: PATCH
query_params:
noparse: '{{$body.input?.noparse}}'
template_engine: Kriti
url: '{{$base_url}}/docs/{{$body.input.docId}}/tables/{{$body.input.tableId}}/data'
version: 2
comment: Deprecated in favor of `records` endpoints. We have no immediate plans to remove these endpoints, but consider `records` a better starting point for new projects.
- name: modifyTables
definition:
kind: synchronous
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
body:
action: transform
template: '{{$body.input.tablesListInput}}'
method: PATCH
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/docs/{{$body.input.docId}}/tables'
version: 2
response_transform:
body:
action: transform
template: '{{$body}}'
template_engine: Kriti
version: 2
- name: modifyWorkspace
definition:
kind: synchronous
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
body:
action: transform
template: '{{$body.input.workspaceParametersInput}}'
method: PATCH
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/workspaces/{{$body.input.workspaceId}}'
version: 2
response_transform:
body:
action: transform
template: '{{$body}}'
template_engine: Kriti
version: 2
- name: modifyWorkspaceAccess
definition:
kind: synchronous
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
body:
action: transform
template: '{{$body.input.workspaceAccessInput}}'
method: PATCH
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/workspaces/{{$body.input.workspaceId}}/access'
version: 2
response_transform:
body:
action: transform
template: '{{$body}}'
template_engine: Kriti
version: 2
- name: moveDoc
definition:
kind: synchronous
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
body:
action: transform
template: '{{$body.input.docMoveInput}}'
method: PATCH
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/docs/{{$body.input.docId}}/move'
version: 2
response_transform:
body:
action: transform
template: '{{$body}}'
template_engine: Kriti
version: 2
- name: replaceColumns
definition:
kind: synchronous
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
body:
action: transform
template: '{{$body.input.updateColumnsInput}}'
method: PUT
query_params:
noadd: '{{$body.input?.noadd}}'
noupdate: '{{$body.input?.noupdate}}'
replaceall: '{{$body.input?.replaceall}}'
template_engine: Kriti
url: '{{$base_url}}/docs/{{$body.input.docId}}/tables/{{$body.input.tableId}}/columns'
version: 2
response_transform:
body:
action: transform
template: '{{$body}}'
template_engine: Kriti
version: 2
- name: replaceRecords
definition:
kind: synchronous
handler: https://data.example.com/api
forward_client_headers: true
headers:
- name: Authorization
value_from_env: GRIST_TOKEN
request_transform:
body:
action: transform
template: '{{$body.input.recordsWithRequireInput}}'
method: PUT
query_params:
allow_empty_require: '{{$body.input?.allow_empty_require}}'
noadd: '{{$body.input?.noadd}}'
noparse: '{{$body.input?.noparse}}'
noupdate: '{{$body.input?.noupdate}}'
onmany: '{{$body.input?.onmany}}'
template_engine: Kriti
url: '{{$base_url}}/docs/{{$body.input.docId}}/tables/{{$body.input.tableId}}/records'
version: 2
response_transform:
body:
action: transform
template: '{{$body}}'
template_engine: Kriti
version: 2
custom_types:
enums:
- name: Access
values:
- description: null
is_deprecated: null
value: owners
- description: null
is_deprecated: null
value: editors
- description: null
is_deprecated: null
value: viewers
- name: Onmany
values:
- description: null
is_deprecated: null
value: first
- description: null
is_deprecated: null
value: none
- description: null
is_deprecated: null
value: all
- name: Type
values:
- description: null
is_deprecated: null
value: Any
- description: null
is_deprecated: null
value: Text
- description: null
is_deprecated: null
value: Numeric
- description: null
is_deprecated: null
value: Int
- description: null
is_deprecated: null
value: Bool
- description: null
is_deprecated: null
value: Date
- description: null
is_deprecated: null
value: DateTimetimezone
- description: null
is_deprecated: null
value: Choice
- description: null
is_deprecated: null
value: ChoiceList
- description: null
is_deprecated: null
value: ReftableId
- description: null
is_deprecated: null
value: RefListtableId
- description: null
is_deprecated: null
value: Attachments
input_objects:
- name: DocParametersInput
- name: DocMoveInput
- name: OrgParametersInput
- name: OrgAccessInput
- name: OrgAccessWriteInput
- name: WorkspaceParametersInput
- name: WorkspaceAccessInput
- name: WorkspaceAccessWriteInput
- name: DocAccessInput
- name: DocAccessWriteInput
- name: Records3ListItemInput
- name: RecordsWithoutIdInput
- name: Records2ListItemInput
- name: RecordsListInput
- name: Records5ListItemInput
- name: RecordsWithRequireInput
- name: ColumnsListItemInput
- name: CreateTablesInput
- name: Tables2ListItemInput
- name: TablesListInput
- name: TablesListItemInput
- name: Columns3ListItemInput
- name: CreateColumnsInput
- name: CreateFieldsInput
- name: Columns5ListItemInput
- name: Fields4Input
- name: UpdateColumnsInput
objects:
- name: Org
- name: User
- name: DocWithWorkspace
- name: WorkspaceWithOrg
- name: OrgAccessRead
- name: UsersListItem
- name: Doc
- name: WorkspaceWithDocsAndDomain
- name: WorkspaceWithDocsAndOrg
- name: Users2ListItem
- name: WorkspaceAccessRead
- name: DocAccessRead
- name: Records2ListItem
- name: RecordsList
- name: Records4ListItem
- name: RecordsWithoutFields
- name: TablesList
- name: TablesListItem
- name: Tables3ListItem
- name: TablesWithoutFields
- name: Columns2ListItem
- name: ColumnsList
- name: GetFields
- name: Columns4ListItem
- name: ColumnsWithoutFields
scalars:
- name: BigInt
- name: JSON
Example queries that actually work:
query Test {
describeWorkspace(workspaceId: 7) {
id
name
org {
id
domain
name
}
docs {
id
name
}
}
}
query Organisations {
listOrgs {
id
domain
name
owner {
id
name
}
}
}
query WorkspaceDocuments {
listWorkspaces(orgId: "7") {
name
id
docs {
id
name
}
}
}
query DescribeTable {
listTables(docId: "bPkMaixKDzP1e5zGufoE4z") {
tables {
id
fields
}
}
}
query ListTableRecords {
listRecords(docId: "bPkMaixKDzP1e5zGufoE4z", tableId: "Phase_I") {
records {
fields
id
}
}
}
Would be interesting to know if mutations also work.
Maybe someone would like to replicate this setup and check if it's usable for certain use cases?
Hi @almereyda, that's interesting! For grist.yml, it would be excellent to get a PR to https://github.com/gristlabs/grist-help that modifies the file such that it passes your validation and also our documentation continue to build correctly.