grist-core icon indicating copy to clipboard operation
grist-core copied to clipboard

Enhancement: GraphQL endpoint

Open jperon opened this issue 2 years ago • 3 comments

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.

jperon avatar Nov 22 '23 14:11 jperon

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

almereyda avatar Jan 30 '24 03:01 almereyda

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?

almereyda avatar Jun 04 '24 13:06 almereyda

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.

paulfitz avatar Jul 31 '24 14:07 paulfitz