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..816bbc0 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.6" 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.6" 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 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..62cb498 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: @@ -78,6 +78,26 @@ components: description: The number of results that should be returned. If no value is provided, 25 results will be returned. example: 25 required: false + per_page_10: + name: per_page + in: query + schema: + type: integer + minimum: 1 + default: 10 + description: The number of results that should be returned. If no value is provided, 10 results will be returned. + example: 10 + required: false + per_page_100: + name: per_page + in: query + schema: + type: integer + minimum: 1 + default: 100 + description: The number of results that should be returned. If no value is provided, 100 results will be returned. + example: 100 + required: false op_type: name: op_type description: >- @@ -2339,9 +2359,17 @@ components: create_row_comment: type: object properties: + row_id: + $ref: "#/components/parameters/row_id" + table_id: + $ref: "#/components/parameters/table_id" comment: type: string example: "Let's discuss this tomorrow" + required: + - row_id + - table_id + - comment generate_snapshot: type: object @@ -2444,7 +2472,7 @@ paths: type: object example: error_message: invalid token - /api-gateway/api/v2/dtables/{base_uuid}/related-users/: + /api/v2.1/dtables/{base_uuid}/related-users/: get: tags: - Base Info @@ -2466,12 +2494,15 @@ paths: user_list: - email: 244b430060f54bb4afa2c2cb7369d244@auth.local name: Ginger Ale - contact_email: gingerale@example.com avatar_url: https://cloud.seatable.io/media/avatars/default.png + id_in_org: W-00026 + name_pinyin: ginger'ale + app_user_list: - email: 8cb2a6da65687600f42905bf1647fd3f@auth.local name: Jasmin Tee - contact_email: jasmintee@example.com avatar_url: https://cloud.seatable.io/media/avatars/default.png + id_in_org: W-00027 + name_pinyin: jasmin'tee # Rows /api-gateway/api/v2/dtables/{base_uuid}/sql/: @@ -4292,7 +4323,7 @@ paths: success:true # Row Comments - /api-gateway/api/v2/dtables/{base_uuid}/comments/: + /api/v2.1/dtables/{base_uuid}/comments/: get: tags: - Row Comments @@ -4305,6 +4336,8 @@ paths: - BaseTokenAuth: [] parameters: - $ref: "#/components/parameters/base_uuid" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/per_page_10" - $ref: "#/components/parameters/row_id" responses: "200": @@ -4312,34 +4345,19 @@ paths: content: application/json: schema: - type: array - items: - type: object + type: object example: - - id: 1 - author: 28d006e7d1754bb4afa2c2cb7369d244@auth.local - comment: This is good! - dtable_uuid: 650d8a0d-7e27-46a8-8b18-6cc6374yf557 - row_id: NAu2B3OcRG6UrWagL-9naA - created_at: "2021-01-15T13:35:26.000Z" - updated_at: "2021-01-15T13:35:26.000Z" - resolved: 0 - - id: 2 - author: 28d006e7d1754bb4afa2c2cb7369d244@auth.local - comment: Go online tomorrow? - dtable_uuid: 650d8a0d-7e27-46a8-8b18-6cc6374yf557 - row_id: NAu2B3OcRG6UrWagL-9naA - created_at: "2021-01-15T13:52:48.000Z" - updated_at: "2021-01-15T13:52:48.000Z" - resolved: 0 - - id: 3 - author: 8cb2a6da1928374ba42905bf1647fd3f@auth.local - comment: Agreed! - dtable_uuid: 650d8a0d-7e27-46a8-8b18-6cc6374yf557 - row_id: NAu2B3OcRG6UrWagL-9naA - created_at: "2021-01-15T13:53:13.000Z" - updated_at: "2021-01-15T13:53:13.000Z" - resolved: 1 + comment_list: + - id: 1 + author: 28d006e7d1754bb4afa2c2cb7369d244@auth.local + comment: This is good! + dtable_uuid: 650d8a0d-7e27-46a8-8b18-6cc6374yf557 + row_id: NAu2B3OcRG6UrWagL-9naA + created_at: "2021-01-15T13:35:26.000Z" + updated_at: "2021-01-15T13:35:26.000Z" + detail: null + resolved: false + count: 1 post: tags: - Row Comments @@ -4356,8 +4374,6 @@ paths: $ref: "#/components/schemas/create_row_comment" parameters: - $ref: "#/components/parameters/base_uuid" - - $ref: "#/components/parameters/table_id" - - $ref: "#/components/parameters/row_id" responses: "200": description: OK @@ -4367,7 +4383,7 @@ paths: type: object example: success:true - /api-gateway/api/v2/dtables/{base_uuid}/comments/{comment_id}/: + /api/v2.1/dtables/{base_uuid}/comments/{comment_id}/: delete: tags: - Row Comments @@ -4388,17 +4404,31 @@ paths: type: object example: success: true - get: + put: tags: - Row Comments - summary: Get Comment - operationId: getComment - description: Get the details of a certain comment with its ID. + summary: Resolve Comment + operationId: resolveComment + description: Update the resolved status of a certain comment by its ID. security: - BaseTokenAuth: [] parameters: - $ref: "#/components/parameters/base_uuid" - $ref: "#/components/parameters/comment_id" + requestBody: + content: + application/json: + schema: + type: object + properties: + options: + type: object + properties: + resolved: + type: integer + enum: [0, 1] + required: + - options responses: "200": description: OK @@ -4406,33 +4436,9 @@ paths: application/json: schema: type: object - properties: - id: - type: integer - author: - type: string - comment: - type: string - dtable_uuid: - type: string - row_id: - type: string - created_at: - type: string - updated_at: - type: string - resolved: - type: integer example: - id: 1 - author: 12345678d17570046f03c2cb7369d244@auth.local - comment: Here is my email address - dtable_uuid: 12345678-7e27-46a8-8b18-6cc6374yf557 - row_id: NAu2B3OcRG6UrWagL-9naA - created_at: "2021-01-15T13:52:48.000Z" - updated_at: "2021-01-15T13:52:48.000Z" - resolved: 0 - /api-gateway/api/v2/dtables/{base_uuid}/comments-count/: + success: true + /api/v2.1/dtables/{base_uuid}/comments-count/: get: tags: - Row Comments @@ -4455,37 +4461,6 @@ paths: type: object example: count: 3 - /api-gateway/api/v2/dtables/{base_uuid}/comments-within-days/: - get: - tags: - - Row Comments - summary: List Comments within Days - operationId: listCommentsWithinDays - description: >- - List all the comments in a base within a given number of days before - today. - security: - - BaseTokenAuth: [] - parameters: - - $ref: "#/components/parameters/base_uuid" - - $ref: "#/components/parameters/days" - responses: - "200": - description: OK - content: - application/json: - schema: - type: object - example: - comments: - - id: 1 - author: 123456786569491ba42905bf1647fd3f@auth.local - comment: Let's discuss this tomorrow - dtable_uuid: 12345678-7e27-46a8-8b18-6cc6f3db2057 - row_id: Qtf7xPmoRaiFyQPO1aNTjA - created_at: "2021-03-09T15:54:22.000Z" - updated_at: "2021-03-09T15:54:22.000Z" - resolved: 0 /api/v2.1/dtables/{base_uuid}/rows-comments-num/: get: tags: @@ -4510,7 +4485,7 @@ paths: C0LWRVCHT0OoAGjoXBuKMA: 1 # Notifications (Base) - /api-gateway/api/v2/dtables/{base_uuid}/notifications/: + /api/v2.1/dtables/{base_uuid}/notifications/: get: tags: - Notifications @@ -4521,6 +4496,8 @@ paths: - BaseTokenAuth: [] parameters: - $ref: "#/components/parameters/base_uuid" + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/per_page" responses: "200": description: OK @@ -4532,6 +4509,7 @@ paths: notification_list: - id: 200 username: 123456786569491ba42905bf1647fd3f@auth.local + dtable_uuid: 650d8a0d-7e27-46a8-8b18-6cc6374yf557 msg_type: row_comment created_at: "2021-02-25T10:38:14.000Z" detail: @@ -4540,22 +4518,12 @@ paths: row_id: fS8qtN6FQ1uPOaNAC0Locw comment: Did you see this? seen: 1 - put: + delete: tags: - Notifications - summary: Mark Base Notifications as seen - operationId: markBaseNotificationsAsSeen - description: Use this request to mark all the notifications as read. - requestBody: - content: - application/x-www-form-urlencoded: - schema: - type: object - properties: - seen: - type: boolean - description: true or false for read or unread. Otherwise invalid. - example: true + summary: Delete Base Notifications + operationId: deleteBaseNotifications + description: Delete all the notifications in the current base irrevocably. security: - BaseTokenAuth: [] parameters: @@ -4569,12 +4537,26 @@ paths: type: object example: success: true - delete: + /api-gateway/api/v2/dtables/{base_uuid}/notifications/: + put: tags: - Notifications - summary: Delete Base Notifications - operationId: deleteBaseNotifications - description: Delete all the notifications in the current base irrevocably. + summary: Mark Base Notifications as seen + operationId: markBaseNotificationsAsSeen + description: Use this request to mark all the notifications as read. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + seen: + type: string + enum: ["true", "false"] + description: '`true` to mark as read and `false` to mark as unread.' + example: "true" + required: + - seen security: - BaseTokenAuth: [] parameters: @@ -4602,9 +4584,12 @@ paths: type: object properties: seen: - type: boolean + type: string + enum: ["true", "false"] description: '`true` to mark as "seen" and `false` as "unseen".' - example: false + example: "true" + required: + - seen security: - BaseTokenAuth: [] parameters: @@ -4665,7 +4650,7 @@ paths: success: true # Activities & Logs - /api-gateway/api/v2/dtables/{base_uuid}/operations/: + /api/v2.1/dtables/{base_uuid}/operation-logs/: get: tags: - Activities & Logs @@ -4676,6 +4661,7 @@ paths: - BaseTokenAuth: [] parameters: - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/per_page_100" - $ref: "#/components/parameters/base_uuid" responses: "200": @@ -4686,7 +4672,9 @@ paths: type: object example: operations: - - author: 12345678d1754bb4afa2c2cb7369d244@auth.local + - id: 118 + dtable_uuid: 650d8a0d-7e27-46a8-8b18-6cc6374yf557 + author: 12345678d1754bb4afa2c2cb7369d244@auth.local app: null op_time: 1610981745927 operation: >- @@ -4704,7 +4692,7 @@ paths: operation: >- {"op_type":"delete_column","table_id":"0000","column_key":"jQyv","old_column":{"rowType":"header","key":"jQyv","type":null,"name":null,"editable":true,"width":200,"resizable":true,"draggable":true,"data":null,"permission_type":"","permitted_users":[],"editor":{"key":null,"ref":null,"props":{},"_owner":null},"formatter":null,"left":480,"idx":3},"upper_column_key":"J2mq"} op_id: 116 - /api-gateway/api/v2/dtables/{base_uuid}/activities/: + /api/v2.1/dtables/{base_uuid}/row-activities/: get: tags: - Activities & Logs @@ -4716,7 +4704,7 @@ paths: parameters: - $ref: "#/components/parameters/row_id" - $ref: "#/components/parameters/page" - - $ref: "#/components/parameters/per_page" + - $ref: "#/components/parameters/per_page_10" - $ref: "#/components/parameters/base_uuid" responses: "200": @@ -4729,14 +4717,17 @@ paths: activities: - id: 6782 dtable_uuid: a57b56d3-1cc5-4ebd-8a6c-a1b28ac3dbdf + row_count: 1 row_id: YMIviMeERQCUiQhPPqo6Gw op_user: 0ef256cb715841dd81b147b2530c2904@auth.local + op_app: null op_type: modify_row op_time: "2021-01-14T09:01:57.000Z" detail: table_id: "0000" table_name: Table1 row_name: Meng + row_name_option: "" row_data: - column_key: BydO column_name: Date @@ -4747,14 +4738,17 @@ paths: old_value: "2020-08-16" - id: 6778 dtable_uuid: a57b56d3-1cc5-4ebd-8a6c-a1b28ac3dbdf + row_count: 1 row_id: YMIviMeERQCUiQhPPqo6Gw op_user: 0ef256cb715841dd81b147b2530c2904@auth.local + op_app: null op_type: modify_row op_time: "2021-01-14T08:56:53.000Z" detail: table_id: "0000" table_name: Table1 row_name: Meng + row_name_option: "" row_data: - column_key: "0000" column_name: Name @@ -4771,14 +4765,17 @@ paths: old_value: "" - id: 5960 dtable_uuid: a57b56d3-1cc5-4ebd-8a6c-a1b28ac3dbdf + row_count: 1 row_id: YMIviMeERQCUiQhPPqo6Gw op_user: 0ef256cb715841dd81b147b2530c2904@auth.local + op_app: null op_type: insert_row op_time: "2020-11-18T12:42:14.000Z" detail: table_id: "0000" table_name: Table1 row_name: "" + row_name_option: "" row_data: - column_key: "0000" column_name: Name 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..7fecbb0 100644 --- a/intro/changelog.md +++ b/intro/changelog.md @@ -14,6 +14,12 @@ 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 + +> 🚧 Breaking changes +> +> - The deprecated API Gateway endpoints for base activity logs, row activities, row comments, collaborators, and base notifications were moved to `/api/v2.1/dtables/{base_uuid}/`. `GET /api-gateway/api/v2/dtables/{base_uuid}/comments-within-days/` was removed; use `GET /api/v2.1/dtables/{base_uuid}/comments/?row_id={row_id}` instead. + ## 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..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/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_comments.py b/tests/test_comments.py index 5d36d6a..8e28d65 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -15,37 +15,12 @@ def _headers(base): def test_listRowComments(base: Base): - """Test listing comments for a row. - - Note: API returns [] (array) when no comments exist, but - {"comments": [...]} when comments exist. The schema says type:object - which is only true when comments exist. We test the empty case here - and accept both formats. - """ + """Test listing comments for a row.""" table_name = 'test_listRowComments' create_table(base, table_name, SIMPLE_COLUMNS) row_ids = append_rows(base, table_name, [{'text': 'target'}]) - import os, requests - server = os.environ['SEATABLE_SERVER'] - resp = requests.get( - f'{server}/api-gateway/api/v2/dtables/{base.uuid}/comments/', - params={'row_id': row_ids[0]}, - headers=_headers(base), - ) - - assert resp.status_code == 200 - data = resp.json() - # Empty: [] or {"comments": []} - assert isinstance(data, (list, dict)) - - -def test_getRowCommentsCount(base: Base): - table_name = 'test_getRowCommentsCount' - create_table(base, table_name, SIMPLE_COLUMNS) - row_ids = append_rows(base, table_name, [{'text': 'target'}]) - - case: Case = base_operations_schema.find_operation_by_id('getRowCommentsCount') \ + case: Case = base_operations_schema.find_operation_by_id('listRowComments') \ .Case( path_parameters={'base_uuid': base.uuid}, query={'row_id': row_ids[0]}, @@ -55,21 +30,26 @@ def test_getRowCommentsCount(base: Base): assert response.status_code == 200 data = response.json() + assert 'comment_list' in data assert 'count' in data -def test_listCommentsWithinDays(base: Base): - case: Case = base_operations_schema.find_operation_by_id('listCommentsWithinDays') \ +def test_getRowCommentsCount(base: Base): + table_name = 'test_getRowCommentsCount' + create_table(base, table_name, SIMPLE_COLUMNS) + row_ids = append_rows(base, table_name, [{'text': 'target'}]) + + case: Case = base_operations_schema.find_operation_by_id('getRowCommentsCount') \ .Case( path_parameters={'base_uuid': base.uuid}, - query={'days': 7}, + query={'row_id': row_ids[0]}, headers=_headers(base), ) response = case.call() assert response.status_code == 200 data = response.json() - assert 'comments' in data + assert 'count' in data def test_getNumberOfComments(base: Base): @@ -91,48 +71,11 @@ def _table_id(base: Base, table_name: str) -> str: def _list_comment_ids(base: Base, row_id: str) -> list[int]: - """createRowComment does not return the new comment's id, so look it up via listRowComments.""" + """createRowComment does not return the new comment's id, so list the row comments.""" case: Case = base_operations_schema.find_operation_by_id('listRowComments') \ .Case(path_parameters={'base_uuid': base.uuid}, query={'row_id': row_id}, headers=_headers(base)) data = case.call().json() - # API returns [] when no comments exist, {"comments": [...]} otherwise. - comments = data['comments'] if isinstance(data, dict) else data - return [c['id'] for c in comments] - - -def test_getComment(base: Base): - table_name = 'test_getComment' - create_table(base, table_name, SIMPLE_COLUMNS) - row_ids = append_rows(base, table_name, [{'text': 'comment target'}]) - - comment_text = 'Test comment from automated tests' - create: Case = base_operations_schema.find_operation_by_id('createRowComment') \ - .Case( - path_parameters={'base_uuid': base.uuid}, - query={'table_id': _table_id(base, table_name), 'row_id': row_ids[0]}, - body={'comment': comment_text}, - headers=_headers(base), - ) - create_response = create.call() - assert create_response.status_code == 200, \ - f'Failed to create comment: {create_response.status_code} {create_response.text}' - - # createRowComment does not return the comment ID, so we need to fetch all comments for this row - comment_ids = _list_comment_ids(base, row_ids[0]) - assert len(comment_ids) == 1 - comment_id = comment_ids[0] - - case: Case = base_operations_schema.find_operation_by_id('getComment') \ - .Case( - path_parameters={'base_uuid': base.uuid, 'comment_id': comment_id}, - headers=_headers(base), - ) - response = case.call() - - assert response.status_code == 200 - data = response.json() - assert data['id'] == comment_id - assert data['comment'] == comment_text + return [comment['id'] for comment in data['comment_list']] def test_deleteComment(base: Base): @@ -143,8 +86,11 @@ def test_deleteComment(base: Base): create: Case = base_operations_schema.find_operation_by_id('createRowComment') \ .Case( path_parameters={'base_uuid': base.uuid}, - query={'table_id': _table_id(base, table_name), 'row_id': row_ids[0]}, - body={'comment': 'Test comment from automated tests'}, + body={ + 'table_id': _table_id(base, table_name), + 'row_id': row_ids[0], + 'comment': 'Test comment from automated tests', + }, headers=_headers(base), ) create_response = create.call() diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 4863991..5773209 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -50,4 +50,5 @@ def test_listCollaborators(base: Base): user = data['user_list'][0] assert 'email' in user assert 'name' in user - assert 'contact_email' in user + assert 'id_in_org' in user + assert 'name_pinyin' in user diff --git a/tests/test_notifications.py b/tests/test_notifications.py index 36ff4c2..47636e3 100644 --- a/tests/test_notifications.py +++ b/tests/test_notifications.py @@ -22,12 +22,11 @@ def test_listBaseNotifications(base: Base): assert 'notification_list' in data -@pytest.mark.xfail(reason="API returns 400 'seen invalid' β€” expects form-encoded string 'true', not JSON boolean") def test_markBaseNotificationsAsSeen(base: Base): case: Case = base_operations_schema.find_operation_by_id('markBaseNotificationsAsSeen') \ .Case( path_parameters={'base_uuid': base.uuid}, - body={'seen': True}, + body={'seen': 'true'}, headers=_headers(base), ) response = case.call() @@ -46,18 +45,14 @@ def test_deleteBaseNotifications(base: Base): assert response.status_code == 200 -@pytest.mark.xfail(reason="API returns 400 'seen invalid' β€” expects form-encoded string 'true', not JSON boolean") def test_markBaseNotificationAsSeen(base: Base): """Mark a single notification as seen. Requires an existing notification_id.""" # First list notifications to get an ID - import os, requests - server = os.environ['SEATABLE_SERVER'] - resp = requests.get( - f'{server}/api-gateway/api/v2/dtables/{base.uuid}/notifications/', - headers=_headers(base), - ) - assert resp.status_code == 200 - notifications = resp.json().get('notification_list', []) + case: Case = base_operations_schema.find_operation_by_id('listBaseNotifications') \ + .Case(path_parameters={'base_uuid': base.uuid}, headers=_headers(base)) + response = case.call() + assert response.status_code == 200 + notifications = response.json().get('notification_list', []) if not notifications: pytest.skip('No notifications available to mark as seen') @@ -67,7 +62,7 @@ def test_markBaseNotificationAsSeen(base: Base): case: Case = base_operations_schema.find_operation_by_id('markBaseNotificationAsSeen') \ .Case( path_parameters={'base_uuid': base.uuid, 'notification_id': notification_id}, - body={'seen': True}, + body={'seen': 'true'}, headers=_headers(base), ) response = case.call() 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/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."