gristlabs / grist-core

Grist is the evolution of spreadsheets.
https://www.getgrist.com/
Apache License 2.0
6.63k stars 292 forks source link

Enhancement: GraphQL endpoint #764

Open jperon opened 8 months ago

jperon commented 8 months ago

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.

almereyda commented 5 months ago

This will also be useful to include Grist databases into GraphQL-federation-backed data meshes.

almereyda commented 1 month ago

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`: ```console 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 # For the Hasura CLI to talk to the correct endpoint HASURA_GRAPHQL_ENDPOINT=http://127.127.0.3:8080 ``` Docker Compose `compose.yml`: ```yaml 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`: ```console 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//${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: ```sh GRIST_TOKEN= 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: ```yaml 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`: ```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`: ```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?