From 7fb5b66e1dc1b463c2f50c726abd82164dbad0eb Mon Sep 17 00:00:00 2001 From: Simon Hammes Date: Thu, 27 Aug 2026 11:47:47 +0200 Subject: [PATCH 1/7] Initialize v7.0 I disabled the jobs that sync any files to readme.com on purpose and cleared out the IDs in .github/readme-ids.json. --- .github/readme-ids.json | 18 +++++++++--------- .github/workflows/rdme-openapi.yml | 4 +++- authentication.yaml | 2 +- base_operations.yaml | 2 +- file_operations.yaml | 2 +- ping_and_info.yaml | 2 +- python-scheduler.yaml | 2 +- system_admin_account_operations.yaml | 2 +- team_admin_account_operations.yaml | 2 +- user_account_operations.yaml | 2 +- 10 files changed, 20 insertions(+), 18 deletions(-) diff --git a/.github/readme-ids.json b/.github/readme-ids.json index d8316d0..864d76c 100644 --- a/.github/readme-ids.json +++ b/.github/readme-ids.json @@ -1,11 +1,11 @@ { - "version": "6.2", - "authentication": "69d4dc0c1422831f8d6fbb8e", - "base_operations": "69d4dc0c1422831f8d6fbb96", - "file_operations": "69d4dc0c1422831f8d6fbb95", - "system_admin_account_operations": "69d4dc0c1422831f8d6fbb92", - "team_admin_account_operations": "69d4dc0c1422831f8d6fbb91", - "user_account_operations": "69d4dc0c1422831f8d6fbb93", - "ping_and_info": "69d4dc0c1422831f8d6fbb90", - "python_scheduler": "69d4dc0c1422831f8d6fbb94" + "version": "7.0", + "authentication": "", + "base_operations": "", + "file_operations": "", + "system_admin_account_operations": "", + "team_admin_account_operations": "", + "user_account_operations": "", + "ping_and_info": "", + "python_scheduler": "" } diff --git a/.github/workflows/rdme-openapi.yml b/.github/workflows/rdme-openapi.yml index 0d266be..79d5b1e 100644 --- a/.github/workflows/rdme-openapi.yml +++ b/.github/workflows/rdme-openapi.yml @@ -30,6 +30,7 @@ jobs: # Job 1: Publish OpenAPI specs and docs to ReadMe.com # ----------------------------------------------------------------------- publish: + if: false # temporarily disabled name: Publish to ReadMe runs-on: ubuntu-latest steps: @@ -175,7 +176,8 @@ jobs: # ----------------------------------------------------------------------- deploy-static: name: Deploy static files - if: github.ref_name == github.event.repository.default_branch + if: false # temporarily disabled + # if: github.ref_name == github.event.repository.default_branch runs-on: ubuntu-latest needs: publish steps: diff --git a/authentication.yaml b/authentication.yaml index a9a3767..90cdd2a 100644 --- a/authentication.yaml +++ b/authentication.yaml @@ -3,7 +3,7 @@ info: title: Authentication description: >- The official SeaTable API Reference (OpenAPI 3.0). - version: "6.2" + version: "7.0" servers: - url: "https://{server}" variables: diff --git a/base_operations.yaml b/base_operations.yaml index 4f4c30d..f8738fb 100644 --- a/base_operations.yaml +++ b/base_operations.yaml @@ -3,7 +3,7 @@ info: title: Base Operations description: >- The official SeaTable API Reference (OpenAPI 3.0). - version: "6.2" + version: "7.0" servers: - url: "https://{server}" variables: diff --git a/file_operations.yaml b/file_operations.yaml index 18abb0c..e0f30e8 100644 --- a/file_operations.yaml +++ b/file_operations.yaml @@ -3,7 +3,7 @@ info: title: File Operations description: >- The official SeaTable API Reference (OpenAPI 3.0). - version: "6.2" + version: "7.0" servers: - url: "https://{server}" variables: diff --git a/ping_and_info.yaml b/ping_and_info.yaml index b103d75..7d98839 100644 --- a/ping_and_info.yaml +++ b/ping_and_info.yaml @@ -3,7 +3,7 @@ info: title: Ping and Server Info description: >- The official SeaTable API Reference (OpenAPI 3.0) - Part "Ping and Server Info". - version: "6.2" + version: "7.0" servers: - url: "https://{server}" variables: diff --git a/python-scheduler.yaml b/python-scheduler.yaml index fc0914e..a32f681 100644 --- a/python-scheduler.yaml +++ b/python-scheduler.yaml @@ -3,7 +3,7 @@ info: title: Python Scheduler description: >- The official SeaTable API Reference (OpenAPI 3.0). - version: "6.2" + version: "7.0" servers: - url: "https://{server}" variables: diff --git a/system_admin_account_operations.yaml b/system_admin_account_operations.yaml index 1faf561..b46227e 100644 --- a/system_admin_account_operations.yaml +++ b/system_admin_account_operations.yaml @@ -3,7 +3,7 @@ info: title: "Account Operations: System admin" description: >- The official SeaTable API Reference (OpenAPI 3.0). - version: "6.2" + version: "7.0" servers: - url: "https://{server}" variables: diff --git a/team_admin_account_operations.yaml b/team_admin_account_operations.yaml index a2b85d1..d5346c7 100644 --- a/team_admin_account_operations.yaml +++ b/team_admin_account_operations.yaml @@ -3,7 +3,7 @@ info: title: Account Operations - Team admin description: >- The official SeaTable API Reference (OpenAPI 3.0). - version: "6.2" + version: "7.0" servers: - url: "https://{server}" variables: diff --git a/user_account_operations.yaml b/user_account_operations.yaml index ee4e22f..2447bc2 100644 --- a/user_account_operations.yaml +++ b/user_account_operations.yaml @@ -3,7 +3,7 @@ info: title: Account Operations - User description: >- The official SeaTable API Reference (OpenAPI 3.0). - version: "6.2" + version: "7.0" servers: - url: "https://{server}" variables: From c21fe953ff018a0da36092cc25ca018df611d5f9 Mon Sep 17 00:00:00 2001 From: Simon Hammes Date: Thu, 27 Aug 2026 12:31:24 +0200 Subject: [PATCH 2/7] Remove init-databases.sql mount --- version-compare/docker-compose.yml | 2 -- version-compare/init-databases.sql | 3 --- 2 files changed, 5 deletions(-) delete mode 100644 version-compare/init-databases.sql diff --git a/version-compare/docker-compose.yml b/version-compare/docker-compose.yml index bcad3fe..0060011 100644 --- a/version-compare/docker-compose.yml +++ b/version-compare/docker-compose.yml @@ -46,8 +46,6 @@ services: - MYSQL_LOG_CONSOLE=true - MARIADB_AUTO_UPGRADE=1 - TZ=${TIME_ZONE} - volumes: - - ./init-databases.sql:/docker-entrypoint-initdb.d/init-databases.sql:ro networks: - backend healthcheck: diff --git a/version-compare/init-databases.sql b/version-compare/init-databases.sql deleted file mode 100644 index 0a040bc..0000000 --- a/version-compare/init-databases.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE DATABASE IF NOT EXISTS `dtable_db`; -CREATE DATABASE IF NOT EXISTS `ccnet_db`; -CREATE DATABASE IF NOT EXISTS `seafile_db`; From 6bc01b886d4ad5abf94995bfbd0bb88df7924591 Mon Sep 17 00:00:00 2001 From: Simon Hammes Date: Mon, 31 Aug 2026 15:02:44 +0200 Subject: [PATCH 3/7] Deploy dtable-server container --- .github/workflows/api-tests.yml | 14 ++- .github/workflows/postman.yml | 2 +- .github/workflows/rdme-openapi.yml | 4 +- .github/workflows/version-compare.yml | 2 +- README.md | 4 + ping_and_info.yaml | 13 +- .../test_getSystemInformation.json | 5 + tests/test_ping.py | 14 --- version-compare/config/seatable-nginx.conf | 113 ++++++++++++++++++ version-compare/docker-compose.yml | 41 ++++++- version-compare/setup.sh | 13 +- 11 files changed, 197 insertions(+), 28 deletions(-) create mode 100644 version-compare/config/seatable-nginx.conf diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml index 3130dca..50f7425 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -15,9 +15,9 @@ on: workflow_dispatch: inputs: version: - description: "SeaTable version (e.g. 6.1.8)" + description: "SeaTable version" required: true - default: "6.1.8" + default: "7.0.5" image: description: "Docker Hub repository" required: true @@ -28,13 +28,14 @@ on: - "seatable/seatable-enterprise-testing" env: - DEFAULT_VERSION: "6.2.12" + DEFAULT_VERSION: "7.0.5" DEFAULT_IMAGE: "seatable/seatable-enterprise-testing" + DTABLE_SERVER_VERSION: "7.0.3-testing" jobs: test: - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 steps: - name: Check out repo uses: actions/checkout@v6 @@ -67,8 +68,11 @@ jobs: - name: Start SeaTable ${{ steps.version.outputs.version }} working-directory: version-compare + env: + SEATABLE_IMAGE: ${{ steps.version.outputs.image }} + SEATABLE_VERSION: ${{ steps.version.outputs.version }} run: | - SEATABLE_IMAGE=${{ steps.version.outputs.image }} SEATABLE_VERSION=${{ steps.version.outputs.version }} docker compose up -d + docker compose up -d ./setup.sh - name: Run API tests diff --git a/.github/workflows/postman.yml b/.github/workflows/postman.yml index 2e00905..5fdefb8 100644 --- a/.github/workflows/postman.yml +++ b/.github/workflows/postman.yml @@ -10,7 +10,7 @@ env: jobs: postman: - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 steps: - name: Checkout repository uses: actions/checkout@v4 diff --git a/.github/workflows/rdme-openapi.yml b/.github/workflows/rdme-openapi.yml index 79d5b1e..b209a05 100644 --- a/.github/workflows/rdme-openapi.yml +++ b/.github/workflows/rdme-openapi.yml @@ -32,7 +32,7 @@ jobs: publish: if: false # temporarily disabled name: Publish to ReadMe - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 steps: - name: Check out repo uses: actions/checkout@v4 @@ -178,7 +178,7 @@ jobs: name: Deploy static files if: false # temporarily disabled # if: github.ref_name == github.event.repository.default_branch - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 needs: publish steps: - name: Check out repo diff --git a/.github/workflows/version-compare.yml b/.github/workflows/version-compare.yml index 3dcc7c7..0abc410 100644 --- a/.github/workflows/version-compare.yml +++ b/.github/workflows/version-compare.yml @@ -38,7 +38,7 @@ on: jobs: compare: - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 steps: - name: Check out repo uses: actions/checkout@v4 diff --git a/README.md b/README.md index a6fd7bf..1b99a74 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,10 @@ cd version-compare export SEATABLE_IMAGE=seatable/seatable-enterprise export SEATABLE_VERSION=6.0.10 +# seatable/dtable-server-js or seatable/dtable-server-go +export DTABLE_SERVER_IMAGE=seatable/dtable-server-js +export DTABLE_SERVER_VERSION=7.0.3-testing + # Create license file cp "SOURCE" seatable-license.txt diff --git a/ping_and_info.yaml b/ping_and_info.yaml index 7d98839..8afe28b 100644 --- a/ping_and_info.yaml +++ b/ping_and_info.yaml @@ -115,10 +115,14 @@ paths: - Ping summary: Ping dtable Server operationId: pingDtableServer - description: >- + description: |- Check the status of the dtable-server. Base related operations are carried out by the dtable-server. These operations are included in the section **Base Operations**. + + > 🚧 Not exposed by default + > + > As of SeaTable 7.0, the default NGINX configuration no longer routes `/dtable-server/` and this endpoint returns `404`. responses: "200": description: Service is available and running @@ -134,7 +138,12 @@ paths: - Ping summary: Ping dtable-db Server operationId: pingDtableDbServer - description: Check the status of the dtable-db server. + description: |- + Check the status of the dtable-db server. + + > 🚧 Not exposed by default + > + > As of SeaTable 7.0, the default NGINX configuration no longer routes `/dtable-db/` and this endpoint returns `404`. responses: "200": description: Service is available and running diff --git a/tests/__snapshots__/test_system_admin/test_getSystemInformation.json b/tests/__snapshots__/test_system_admin/test_getSystemInformation.json index aa74e0d..5848b8f 100644 --- a/tests/__snapshots__/test_system_admin/test_getSystemInformation.json +++ b/tests/__snapshots__/test_system_admin/test_getSystemInformation.json @@ -5,11 +5,16 @@ "archived_row_count": "int", "dtable_server_info": [ { + "assigned_keys_count": 0, + "backend": "", "enable_cluster": false, "last_dtable_saving_count": "int", "last_period_operations_count": "int", "loaded_dtables_count": "int", + "local_node_url": "http://dtable-server:5000", + "node_id": "", "operation_count_since_up": "int", + "start_time": "", "web_socket_count": "int" } ], diff --git a/tests/test_ping.py b/tests/test_ping.py index 56f67e9..94e5bfb 100644 --- a/tests/test_ping.py +++ b/tests/test_ping.py @@ -30,20 +30,6 @@ def test_pingServerWithAuth(account_token: Secret): assert response.status_code == 200 assert response.text.strip('"') == 'pong' -def test_pingDtableServer(): - case: Case = ping_and_info_schema.find_operation_by_id('pingDtableServer').Case() - response = case.call() - - assert response.status_code == 200 - assert response.text.strip() == 'pong' - -def test_pingDtableDbServer(): - case: Case = ping_and_info_schema.find_operation_by_id('pingDtableDbServer').Case() - response = case.call() - - assert response.status_code == 200 - assert response.json()['ret'] == 'pong' - def test_pingApiGateway(): case: Case = ping_and_info_schema.find_operation_by_id('pingApiGateway').Case() response = case.call() diff --git a/version-compare/config/seatable-nginx.conf b/version-compare/config/seatable-nginx.conf new file mode 100644 index 0000000..cb7fe73 --- /dev/null +++ b/version-compare/config/seatable-nginx.conf @@ -0,0 +1,113 @@ +log_format seatableformat '\$http_x_forwarded_for \$remote_addr [\$time_local] "\$request" \$status \$body_bytes_sent "\$http_referer" "\$http_user_agent" \$upstream_response_time'; + +server { + listen 80; + listen [::]:80; + server_name _; + + proxy_set_header X-Forwarded-For $remote_addr; + + # CORS settings to allow API access from api.seatable.com + proxy_hide_header 'Access-Control-Allow-Origin'; + add_header 'Access-Control-Allow-Origin' 'https://api.seatable.com' always; + add_header 'Access-Control-Allow-Methods' 'GET,POST,PUT,DELETE,OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Content-Type, Accept, authorization, token, deviceType, x-seafile-otp' always; + if ($request_method = 'OPTIONS') { + return 204; + } + + location / { + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + proxy_read_timeout 1200s; + client_max_body_size 0; + + access_log /opt/nginx-logs/dtable-web.access.log seatableformat; + error_log /opt/nginx-logs/dtable-web.error.log; + } + + location /seafhttp { + rewrite ^/seafhttp(.*)$ $1 break; + proxy_pass http://127.0.0.1:8082; + + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_request_buffering off; + proxy_connect_timeout 36000s; + proxy_read_timeout 36000s; + proxy_send_timeout 36000s; + send_timeout 36000s; + client_max_body_size 0; + + access_log /opt/nginx-logs/seafhttp.access.log seatableformat; + error_log /opt/nginx-logs/seafhttp.error.log; + } + + location /media { + root /opt/seatable/seatable-server-latest/dtable-web; + access_log off; + error_log off; + } + + location /api-gateway/ { + proxy_pass http://127.0.0.1:7780/; + proxy_redirect off; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_hide_header Access-Control-Allow-Origin; + proxy_hide_header Access-Control-Allow-Methods; + proxy_hide_header Access-Control-Allow-Headers; + + client_max_body_size 10m; + + access_log /opt/nginx-logs/api-gateway.access.log seatableformat; + error_log /opt/nginx-logs/api-gateway.error.log; + } + + location /api-gateway/socket.io/ { + proxy_pass http://127.0.0.1:7780/socket.io/; + + # websocket proxying + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection upgrade; + + proxy_redirect off; + proxy_buffers 8 32k; + proxy_buffer_size 64k; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_set_header X-NginX-Proxy true; + + access_log /opt/nginx-logs/socket-io.access.log seatableformat; + error_log /opt/nginx-logs/socket-io.error.log; + } + + location /internal-thumbnails/ { + internal; + alias /opt/seatable/seahub-data/thumbnail/; + + add_header X-Served-By "Nginx-via-X-Accel"; + expires 7d; + add_header Cache-Control "public, no-transform"; + + access_log /opt/nginx-logs/internal-thumbnails.access.log seatableformat; + error_log /opt/nginx-logs/internal-thumbnails.error.log; + } + + # Deprecated endpoints + location /dtable-server { + return 404 'This endpoint is deprecated. Please migrate to the API Gateway: https://forum.seatable.com/t/important-changes-to-the-seatable-cloud-api-in-version-5-3/6677'; + } + + location /dtable-db { + return 404 'This endpoint is deprecated. Please migrate to the API Gateway: https://forum.seatable.com/t/important-changes-to-the-seatable-cloud-api-in-version-5-3/6677'; + } +} diff --git a/version-compare/docker-compose.yml b/version-compare/docker-compose.yml index 0060011..7dae2c5 100644 --- a/version-compare/docker-compose.yml +++ b/version-compare/docker-compose.yml @@ -20,8 +20,10 @@ services: - SEATABLE_ADMIN_EMAIL=admin@example.com - SEATABLE_ADMIN_PASSWORD=admin1234 - TIME_ZONE=Europe/Berlin + - INNER_DTABLE_SERVER_URL=http://dtable-server:5000 volumes: - ./seatable-data:/shared + - ./config/seatable-nginx.conf:/etc/nginx/sites-enabled/default - type: bind source: ./seatable-license.txt target: /shared/seatable/seatable-license.txt @@ -37,8 +39,43 @@ services: networks: - backend + dtable-server: + image: ${DTABLE_SERVER_IMAGE:-seatable/dtable-server-js}:${DTABLE_SERVER_VERSION:?Variable is not set} + container_name: dtable-server + restart: unless-stopped + environment: + - SEATABLE_MYSQL_DB_HOST=mariadb + - SEATABLE_MYSQL_DB_USER=root + - SEATABLE_MYSQL_DB_PORT=3306 + - SEATABLE_MYSQL_DB_PASSWORD=seatable + - SEATABLE_MYSQL_DB_DTABLE_DB_NAME=dtable_db + - SEATABLE_MYSQL_DB_CCNET_DB_NAME=ccnet_db + - SEATABLE_MYSQL_DB_SEAFILE_DB_NAME=seafile_db + - REDIS_HOST=redis + - REDIS_PORT=6379 + - REDIS_PASSWORD=seatable + - JWT_PRIVATE_KEY=test-jwt-private-key-for-ci + - TIME_ZONE=Europe/Berlin + - TZ=Europe/Berlin + - INNER_DTABLE_WEB_SERVICE_URL=http://seatable-server:80 + - INNER_DTABLE_DB_URL=http://seatable-server:7777 + volumes: + - ./seatable-data:/shared + - type: bind + source: ./seatable-license.txt + target: /shared/seatable/seatable-license.txt + bind: + create_host_path: false + depends_on: + mariadb: + condition: service_healthy + redis: + condition: service_healthy + networks: + - backend + mariadb: - image: mariadb:11.8.3-noble + image: mariadb:11.8.5-noble container_name: mariadb command: ["mariadbd", "--innodb_snapshot_isolation=OFF"] environment: @@ -56,7 +93,7 @@ services: timeout: 10s redis: - image: redis:8.2.2-bookworm + image: redis:8.4.0-bookworm container_name: redis command: ["redis-server", "--requirepass", "seatable"] networks: diff --git a/version-compare/setup.sh b/version-compare/setup.sh index ea533a4..ca315d1 100755 --- a/version-compare/setup.sh +++ b/version-compare/setup.sh @@ -18,7 +18,7 @@ echo "Waiting for SeaTable to become available..." start_time=$(date +%s) while true; do - if curl -sf "${SEATABLE_SERVER}/dtable-server/ping/" > /dev/null 2>&1; then + if curl -sf "${SEATABLE_SERVER}/api-gateway/api/v2/ping/" > /dev/null 2>&1; then echo "SeaTable is ready." break fi @@ -197,4 +197,15 @@ SETTINGS echo "Restarting SeaTable to apply settings..." docker exec seatable-server /templates/seatable.sh restart +# FIXME: dtable-server boots in parallel with seatable-server and only symlinks +# /opt/seatable/storage-data -> /shared/seatable/storage-data if that directory +# already exists. On a fresh data directory it does not, so dtable-server keeps a +# container-local storage-data, cannot find any base, and every base operation +# fails with HTTP 500. The container must be *recreated* (not just restarted) β€” +# a restart keeps the local directory and the symlink step fails with +# "cannot overwrite directory". Remove once dtable-server creates the symlink +# unconditionally. +echo "Recreating dtable-server..." +docker compose up -d --force-recreate dtable-server + echo "Setup complete." From 0c641e095d227cee37535f91e8074c072b0f77b7 Mon Sep 17 00:00:00 2001 From: Simon Hammes Date: Wed, 2 Sep 2026 11:42:26 +0200 Subject: [PATCH 4/7] Bump version to v7.0.6 --- .github/workflows/api-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml index 50f7425..816bbc0 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -17,7 +17,7 @@ on: version: description: "SeaTable version" required: true - default: "7.0.5" + default: "7.0.6" image: description: "Docker Hub repository" required: true @@ -28,7 +28,7 @@ on: - "seatable/seatable-enterprise-testing" env: - DEFAULT_VERSION: "7.0.5" + DEFAULT_VERSION: "7.0.6" DEFAULT_IMAGE: "seatable/seatable-enterprise-testing" DTABLE_SERVER_VERSION: "7.0.3-testing" From a556385c5f13416a7115b849b140a0433154ac9b Mon Sep 17 00:00:00 2001 From: Simon Hammes Date: Fri, 11 Sep 2026 11:22:39 +0200 Subject: [PATCH 5/7] Bump image tags to 7.0.7 --- .github/workflows/api-tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml index 816bbc0..ff52e41 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -17,7 +17,7 @@ on: version: description: "SeaTable version" required: true - default: "7.0.6" + default: "7.0.7" image: description: "Docker Hub repository" required: true @@ -28,9 +28,9 @@ on: - "seatable/seatable-enterprise-testing" env: - DEFAULT_VERSION: "7.0.6" + DEFAULT_VERSION: "7.0.7" DEFAULT_IMAGE: "seatable/seatable-enterprise-testing" - DTABLE_SERVER_VERSION: "7.0.3-testing" + DTABLE_SERVER_VERSION: "7.0.7-testing" jobs: From d7be79f9fb7e755f0d22fd3b982e37becbc022cf Mon Sep 17 00:00:00 2001 From: Simon Hammes Date: Fri, 11 Sep 2026 15:24:20 +0200 Subject: [PATCH 6/7] Document new dtable-web endpoints --- intro/changelog.md | 10 ++ team_admin_account_operations.yaml | 124 ++++++++++++++ .../test_team_apps/test_listManagedApps.json | 20 +++ .../test_team_apps/test_listUsableApps.json | 20 +++ .../test_listBasesSharedToUser.json | 25 +++ tests/test_team_apps.py | 161 ++++++++++++++++++ tests/test_team_bases.py | 84 +++++++++ 7 files changed, 444 insertions(+) create mode 100644 tests/__snapshots__/test_team_apps/test_listManagedApps.json create mode 100644 tests/__snapshots__/test_team_apps/test_listUsableApps.json create mode 100644 tests/__snapshots__/test_team_bases/test_listBasesSharedToUser.json create mode 100644 tests/test_team_apps.py create mode 100644 tests/test_team_bases.py diff --git a/intro/changelog.md b/intro/changelog.md index a4a0a63..71f78b4 100644 --- a/intro/changelog.md +++ b/intro/changelog.md @@ -14,6 +14,16 @@ slug: changelog Listed below are all the changes to the SeaTable API. Each date corresponds to a new version of SeaTable Server Enterprise Edition. If you’re looking for changes beyond the API, see the SeaTable [Changelog](https://seatable.com/changelog) or check out the [SeaTable Blog](https://seatable.com/blog) for detailed release notes. +## Version 7.0 + +> πŸ“˜ New requests +> +> **Account Operations - Team Admin** +> +> - [List Managed Apps](/reference/listmanagedapps) get +> - [List Usable Apps](/reference/listusableapps) get +> - [List Bases Shared to User](/reference/listbasessharedtouser-1) get + ## Version 6.2 (21.07.2026) > 🚧 Breaking changes diff --git a/team_admin_account_operations.yaml b/team_admin_account_operations.yaml index d5346c7..22c623d 100644 --- a/team_admin_account_operations.yaml +++ b/team_admin_account_operations.yaml @@ -715,6 +715,88 @@ paths: example: success: true + /api/v2.1/org/{org_id}/admin/users/{user_id}/managed-external-apps/: + get: + tags: + - Users + summary: List Managed Apps + operationId: listManagedApps + description: >- + List all the universal apps a certain user manages, i.e. the apps in + which the user has the `admin` role. Inactive apps and apps of deleted + bases are not included. + security: + - AccountTokenAuth: [] + parameters: + - $ref: "#/components/parameters/org_id" + - $ref: "#/components/parameters/user_id" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/per_page" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + example: + managed_apps: + - app_user_id: 12 + app_id: 4 + app_name: Customer Portal + app_uuid: 25ed3722-7988-4389-8095-9a46373cd8a5 + app_config: '{"app_type": "universal-app", "app_name": "Customer Portal", "settings": {"pages": [], "navigation": []}}' + role_name: admin + permission: rw + dtable_uuid: c56624fd757f4f4bae7de76235aeb1f0 + dtable_name: CRM + workspace_id: 2 + link: https://cloud.seatable.io/external-apps/25ed3722-7988-4389-8095-9a46373cd8a5/ + edit_link: https://cloud.seatable.io/dtable/external-apps-edit/25ed3722-7988-4389-8095-9a46373cd8a5/ + joined_at: '2025-11-28T14:16:30+01:00' + count: 1 + + /api/v2.1/org/{org_id}/admin/users/{user_id}/can-use-external-apps/: + get: + tags: + - Users + summary: List Usable Apps + operationId: listUsableApps + description: >- + List all the universal apps a certain user can use, i.e. the apps in + which the user has a role other than `admin`. Inactive apps and apps + of deleted bases are not included. + security: + - AccountTokenAuth: [] + parameters: + - $ref: "#/components/parameters/org_id" + - $ref: "#/components/parameters/user_id" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/per_page" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + example: + can_use_apps: + - app_user_id: 13 + app_id: 4 + app_name: Customer Portal + app_uuid: 25ed3722-7988-4389-8095-9a46373cd8a5 + app_config: '{"app_type": "universal-app", "app_name": "Customer Portal", "settings": {"pages": [], "navigation": []}}' + role_name: default + permission: rw + dtable_uuid: c56624fd757f4f4bae7de76235aeb1f0 + dtable_name: CRM + workspace_id: 2 + link: https://cloud.seatable.io/external-apps/25ed3722-7988-4389-8095-9a46373cd8a5/ + edit_link: https://cloud.seatable.io/dtable/external-apps-edit/25ed3722-7988-4389-8095-9a46373cd8a5/ + joined_at: '2025-11-28T14:16:30+01:00' + count: 1 + # Bases /api/v2.1/org/{org_id}/admin/dtables/: get: @@ -837,6 +919,48 @@ paths: owner_deleted: false rows_count: 0 count: 2 + /api/v2.1/org/{org_id}/admin/users/{user_id}/shared-dtables/: + get: + tags: + - Bases + summary: List Bases Shared to User + operationId: listBasesSharedToUser + description: List all the bases shared to a certain user by the user's ID. + security: + - AccountTokenAuth: [] + parameters: + - $ref: "#/components/parameters/org_id" + - $ref: "#/components/parameters/user_id" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/per_page" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + example: + dtable_list: + - id: 8 + workspace_id: 2 + uuid: 3a6e34b7-c9e1-4c2f-8b0e-2f5d3f21c7a4 + name: CRM + created_at: '2025-11-28T14:16:30+01:00' + updated_at: '2025-11-28T14:17:04+01:00' + color: "#E91E63" + text_color: null + icon: icon-user-interview + is_encrypted: false + in_storage: true + backend: js + creator: org-1-admin + modifier: org-1-admin + file_size: 1389 + rows_count: 7 + from_user: e38487b4357e40c8b1359223ba87a1af@auth.local + from_user_name: org-1-admin + count: 1 /api/v2.1/org/{org_id}/admin/dtables/{base_uuid}/: get: tags: diff --git a/tests/__snapshots__/test_team_apps/test_listManagedApps.json b/tests/__snapshots__/test_team_apps/test_listManagedApps.json new file mode 100644 index 0000000..b10455e --- /dev/null +++ b/tests/__snapshots__/test_team_apps/test_listManagedApps.json @@ -0,0 +1,20 @@ +{ + "count": 1, + "managed_apps": [ + { + "app_config": "{\"app_type\": \"universal-app\", \"app_name\": \"Test App\", \"settings\": {\"pages\": [], \"navigation\": []}}", + "app_id": "int", + "app_name": "Test App", + "app_user_id": "int", + "app_uuid": "str", + "dtable_name": "ManagedAppsTest", + "dtable_uuid": "str", + "edit_link": "str", + "joined_at": "str", + "link": "str", + "permission": "rw", + "role_name": "admin", + "workspace_id": "int" + } + ] +} diff --git a/tests/__snapshots__/test_team_apps/test_listUsableApps.json b/tests/__snapshots__/test_team_apps/test_listUsableApps.json new file mode 100644 index 0000000..0e994ff --- /dev/null +++ b/tests/__snapshots__/test_team_apps/test_listUsableApps.json @@ -0,0 +1,20 @@ +{ + "can_use_apps": [ + { + "app_config": "{\"app_type\": \"universal-app\", \"app_name\": \"Test App\", \"settings\": {\"pages\": [], \"navigation\": []}}", + "app_id": "int", + "app_name": "Test App", + "app_user_id": "int", + "app_uuid": "str", + "dtable_name": "UsableAppsTest", + "dtable_uuid": "str", + "edit_link": "str", + "joined_at": "str", + "link": "str", + "permission": "rw", + "role_name": "default", + "workspace_id": "int" + } + ], + "count": 1 +} diff --git a/tests/__snapshots__/test_team_bases/test_listBasesSharedToUser.json b/tests/__snapshots__/test_team_bases/test_listBasesSharedToUser.json new file mode 100644 index 0000000..133785c --- /dev/null +++ b/tests/__snapshots__/test_team_bases/test_listBasesSharedToUser.json @@ -0,0 +1,25 @@ +{ + "count": 1, + "dtable_list": [ + { + "backend": "js", + "color": null, + "created_at": "str", + "creator": "str", + "file_size": "int", + "from_user": "str", + "from_user_name": "str", + "icon": null, + "id": "int", + "in_storage": true, + "is_encrypted": false, + "modifier": "str", + "name": "SharedBasesTest", + "rows_count": 0, + "text_color": null, + "updated_at": "str", + "uuid": "str", + "workspace_id": "int" + } + ] +} diff --git a/tests/test_team_apps.py b/tests/test_team_apps.py new file mode 100644 index 0000000..2e9764a --- /dev/null +++ b/tests/test_team_apps.py @@ -0,0 +1,161 @@ +import json +import pytest +import requests +from conftest import ( + BASE_URL, Secret, TeamAdmin, system_admin_account_operations, team_admin_account_operations, + user_account_operations, +) +from schemathesis import Case +from syrupy.assertion import SnapshotAssertion +from syrupy.matchers import path_type + +pytestmark = pytest.mark.needs_large_license + + +def test_listManagedApps(team: TeamAdmin, system_admin_account_token: Secret, snapshot_json: SnapshotAssertion): + """Create a universal app, make the team admin its admin and verify it appears in listManagedApps.""" + headers = {'Authorization': f'Bearer {team.account_token}'} + path_parameters = {'org_id': team.team_id} + + # Custom app roles require the "advanced customization" permission, which the default team role lacks + case: Case = system_admin_account_operations.find_operation_by_id('updateTeam') \ + .Case(path_parameters=path_parameters, body={'role': 'org_enterprise'}) + response = case.call(headers={'Authorization': f'Bearer {system_admin_account_token.value}'}) + assert response.status_code == 200 + + # The team admin will be the app's admin, so get their own user ID + case: Case = user_account_operations.find_operation_by_id('getAccountInfo').Case() + response = case.call(headers=headers) + assert response.status_code == 200 + user_id = response.json()['email'] + + # Create a base in the team admin's personal workspace + case: Case = user_account_operations.find_operation_by_id('listWorkspaces').Case() + response = case.call(headers=headers) + assert response.status_code == 200 + ws_id = next(w for w in response.json()['workspace_list'] if w.get('type') == 'personal')['id'] + + body = {'workspace_id': ws_id, 'name': 'ManagedAppsTest'} + case: Case = user_account_operations.find_operation_by_id('createBase').Case(body=body) + response = case.call(headers=headers) + assert response.status_code == 201 + base_uuid = response.json()['table']['uuid'] + + # Create a universal app, an "admin" role and add the team admin with that role (endpoints not in the spec) + app_config = json.dumps({'app_type': 'universal-app', 'app_name': 'Test App', 'settings': {'pages': [], 'navigation': []}}) + response = requests.post( + f'{BASE_URL}/api/v2.1/workspace/{ws_id}/dtable/ManagedAppsTest/external-apps/', + headers=headers, data={'app_type': 'universal-app', 'app_config': app_config}, + ) + assert response.status_code == 201 + app_uuid = response.json()['external_app']['app_uuid'] + + response = requests.post( + f'{BASE_URL}/api/v2.1/universal-apps/{app_uuid}/app-roles/', + headers=headers, data={'role_name': 'admin', 'permission': 'rw'}, + ) + assert response.status_code == 200 + role_id = response.json()['app_role']['id'] + + response = requests.post( + f'{BASE_URL}/api/v2.1/universal-apps/{app_uuid}/app-users/', + headers=headers, data={'app_user': user_id, 'app_role_id': role_id}, + ) + assert response.status_code == 200 + + # List the user's managed apps + case: Case = team_admin_account_operations.find_operation_by_id('listManagedApps') \ + .Case(path_parameters={'org_id': team.team_id, 'user_id': user_id}) + response = case.call(headers=headers) + + assert response.status_code == 200 + + data = response.json() + assert data['count'] == 1 + assert len(data['managed_apps']) == 1 + + managed_app = data['managed_apps'][0] + assert managed_app['app_uuid'] == app_uuid + assert managed_app['dtable_uuid'] == base_uuid.replace('-', '') + + matcher = path_type({ + r"managed_apps\.\d+\.app_user_id": (int,), + r"managed_apps\.\d+\.app_id": (int,), + r"managed_apps\.\d+\.app_uuid": (str,), + r"managed_apps\.\d+\.dtable_uuid": (str,), + r"managed_apps\.\d+\.workspace_id": (int,), + r"managed_apps\.\d+\.link": (str,), + r"managed_apps\.\d+\.edit_link": (str,), + r"managed_apps\.\d+\.joined_at": (str,), + }, regex=True) + + assert snapshot_json(matcher=matcher) == data + + +def test_listUsableApps(team: TeamAdmin, snapshot_json: SnapshotAssertion): + """Create a universal app, add the team admin with the default role and verify it appears in listUsableApps.""" + headers = {'Authorization': f'Bearer {team.account_token}'} + + case: Case = user_account_operations.find_operation_by_id('getAccountInfo').Case() + response = case.call(headers=headers) + assert response.status_code == 200 + user_id = response.json()['email'] + + # Create a base in the team admin's personal workspace + case: Case = user_account_operations.find_operation_by_id('listWorkspaces').Case() + response = case.call(headers=headers) + assert response.status_code == 200 + ws_id = next(w for w in response.json()['workspace_list'] if w.get('type') == 'personal')['id'] + + body = {'workspace_id': ws_id, 'name': 'UsableAppsTest'} + case: Case = user_account_operations.find_operation_by_id('createBase').Case(body=body) + response = case.call(headers=headers) + assert response.status_code == 201 + base_uuid = response.json()['table']['uuid'] + + # Create a universal app and add the team admin with its auto-created "default" role (endpoints not in the spec) + app_config = json.dumps({'app_type': 'universal-app', 'app_name': 'Test App', 'settings': {'pages': [], 'navigation': []}}) + response = requests.post( + f'{BASE_URL}/api/v2.1/workspace/{ws_id}/dtable/UsableAppsTest/external-apps/', + headers=headers, data={'app_type': 'universal-app', 'app_config': app_config}, + ) + assert response.status_code == 201 + app_uuid = response.json()['external_app']['app_uuid'] + + response = requests.get(f'{BASE_URL}/api/v2.1/universal-apps/{app_uuid}/app-roles/', headers=headers) + assert response.status_code == 200 + role_id = next(r['id'] for r in response.json()['app_roles'] if r['role_name'] == 'default') + + response = requests.post( + f'{BASE_URL}/api/v2.1/universal-apps/{app_uuid}/app-users/', + headers=headers, data={'app_user': user_id, 'app_role_id': role_id}, + ) + assert response.status_code == 200 + + # List the apps the user can use + case: Case = team_admin_account_operations.find_operation_by_id('listUsableApps') \ + .Case(path_parameters={'org_id': team.team_id, 'user_id': user_id}) + response = case.call(headers=headers) + + assert response.status_code == 200 + + data = response.json() + assert data['count'] == 1 + assert len(data['can_use_apps']) == 1 + + usable_app = data['can_use_apps'][0] + assert usable_app['app_uuid'] == app_uuid + assert usable_app['dtable_uuid'] == base_uuid.replace('-', '') + + matcher = path_type({ + r"can_use_apps\.\d+\.app_user_id": (int,), + r"can_use_apps\.\d+\.app_id": (int,), + r"can_use_apps\.\d+\.app_uuid": (str,), + r"can_use_apps\.\d+\.dtable_uuid": (str,), + r"can_use_apps\.\d+\.workspace_id": (int,), + r"can_use_apps\.\d+\.link": (str,), + r"can_use_apps\.\d+\.edit_link": (str,), + r"can_use_apps\.\d+\.joined_at": (str,), + }, regex=True) + + assert snapshot_json(matcher=matcher) == data diff --git a/tests/test_team_bases.py b/tests/test_team_bases.py new file mode 100644 index 0000000..52545cd --- /dev/null +++ b/tests/test_team_bases.py @@ -0,0 +1,84 @@ +import pytest +from conftest import ( + TeamAdmin, team_admin_account_operations, user_account_operations, generate_password, +) +from random import randint +from schemathesis import Case +from syrupy.assertion import SnapshotAssertion +from syrupy.matchers import path_type + +pytestmark = pytest.mark.needs_large_license + + +def test_listBasesSharedToUser(team: TeamAdmin, snapshot_json: SnapshotAssertion): + """Share a base from the team admin to a team member and verify it appears in listBasesSharedToUser.""" + headers = {'Authorization': f'Bearer {team.account_token}'} + path_parameters = {'org_id': team.team_id} + + # Add a user to the team (emails are reserved forever, so use a unique one per run) + body = { + 'email': f'shared-bases-test-{randint(1, 1000000)}@example.com', + 'name': 'Shared Bases Test', + 'password': generate_password(), + 'with_workspace': True, + } + case: Case = team_admin_account_operations.find_operation_by_id('addUser') \ + .Case(path_parameters=path_parameters, body=body) + response = case.call(headers=headers) + assert response.status_code == 200 + user_id = response.json()['email'] + + # Get the team admin's own user ID and personal workspace + case: Case = user_account_operations.find_operation_by_id('getAccountInfo').Case() + response = case.call(headers=headers) + assert response.status_code == 200 + admin_user_id = response.json()['email'] + + case: Case = user_account_operations.find_operation_by_id('listWorkspaces').Case() + response = case.call(headers=headers) + assert response.status_code == 200 + ws_id = next(w for w in response.json()['workspace_list'] if w.get('type') == 'personal')['id'] + + body = {'workspace_id': ws_id, 'name': 'SharedBasesTest'} + case: Case = user_account_operations.find_operation_by_id('createBase').Case(body=body) + response = case.call(headers=headers) + assert response.status_code == 201 + base_uuid = response.json()['table']['uuid'] + + # Share the base to the new user + body = {'email': user_id, 'permission': 'r'} + case: Case = user_account_operations.find_operation_by_id('createUserShare') \ + .Case(path_parameters={'workspace_id': ws_id, 'base_name': 'SharedBasesTest'}, body=body) + response = case.call(headers=headers) + assert response.status_code == 201 + + # List bases shared to the user + case: Case = team_admin_account_operations.find_operation_by_id('listBasesSharedToUser') \ + .Case(path_parameters={'org_id': team.team_id, 'user_id': user_id}) + response = case.call(headers=headers) + + assert response.status_code == 200 + + data = response.json() + assert data['count'] == 1 + assert len(data['dtable_list']) == 1 + + shared_base = data['dtable_list'][0] + assert shared_base['uuid'] == base_uuid + assert shared_base['from_user'] == admin_user_id + + matcher = path_type({ + r"dtable_list\.\d+\.id": (int,), + r"dtable_list\.\d+\.workspace_id": (int,), + r"dtable_list\.\d+\.uuid": (str,), + r"dtable_list\.\d+\.created_at": (str,), + r"dtable_list\.\d+\.updated_at": (str,), + r"dtable_list\.\d+\.creator": (str,), + r"dtable_list\.\d+\.modifier": (str,), + r"dtable_list\.\d+\.file_size": (int,), + r"dtable_list\.\d+\.from_user": (str,), + r"dtable_list\.\d+\.from_user_name": (str,), + }, regex=True) + + assert snapshot_json(matcher=matcher) == data + From 362b39eba4be2c0b8024afa74d17ad8757bb3072 Mon Sep 17 00:00:00 2001 From: Simon Hammes Date: Fri, 11 Sep 2026 15:33:15 +0200 Subject: [PATCH 7/7] API returns last_login = null when creating a user --- team_admin_account_operations.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/team_admin_account_operations.yaml b/team_admin_account_operations.yaml index 22c623d..ac79657 100644 --- a/team_admin_account_operations.yaml +++ b/team_admin_account_operations.yaml @@ -451,6 +451,7 @@ paths: type: string last_login: type: string + nullable: true self_usage: type: integer quota: