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/api-tests.yml b/.github/workflows/api-tests.yml
index 3130dca..ff52e41 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.7"
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.7"
DEFAULT_IMAGE: "seatable/seatable-enterprise-testing"
+ DTABLE_SERVER_VERSION: "7.0.7-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 0d266be..b209a05 100644
--- a/.github/workflows/rdme-openapi.yml
+++ b/.github/workflows/rdme-openapi.yml
@@ -30,8 +30,9 @@ jobs:
# Job 1: Publish OpenAPI specs and docs to ReadMe.com
# -----------------------------------------------------------------------
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
@@ -175,8 +176,9 @@ jobs:
# -----------------------------------------------------------------------
deploy-static:
name: Deploy static files
- if: github.ref_name == github.event.repository.default_branch
- runs-on: ubuntu-latest
+ if: false # temporarily disabled
+ # if: github.ref_name == github.event.repository.default_branch
+ 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/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/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/ping_and_info.yaml b/ping_and_info.yaml
index b103d75..8afe28b 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:
@@ -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/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..ac79657 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:
@@ -451,6 +451,7 @@ paths:
type: string
last_login:
type: string
+ nullable: true
self_usage:
type: integer
quota:
@@ -715,6 +716,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 +920,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_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/__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_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/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
+
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:
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 bcad3fe..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:
@@ -46,8 +83,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:
@@ -58,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/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`;
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."