From 956b93fe9dfd96e22db9808ebab67fbae25cca36 Mon Sep 17 00:00:00 2001 From: Karl Bauer Date: Fri, 10 Oct 2025 11:18:16 +0200 Subject: [PATCH 1/7] feat: Add support for benchmark tests in run-all script and quick-test script --- pyproject.toml | 1 + scripts/quick-test.py | 12 +++++++-- scripts/run-all.py | 60 +++++++++++++++++++++++++++++++------------ 3 files changed, 55 insertions(+), 18 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2875d54..44ead1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -171,6 +171,7 @@ markers = [ "unit: Unit tests", "integration: Integration tests", "slow: Slow running tests", + "performance: Performance tests", "benchmark: Benchmark tests", "asyncio: Asynchronous tests", ] diff --git a/scripts/quick-test.py b/scripts/quick-test.py index d62bf4f..a8aa9cb 100644 --- a/scripts/quick-test.py +++ b/scripts/quick-test.py @@ -86,9 +86,17 @@ def main(): ), # Type checking (most important files only) (["python", "-m", "mypy", "src/nocodb_simple_client/__init__.py"], "Quick type check"), - # Fast tests only + # Fast tests only (exclude slow, integration, performance, and benchmark tests) ( - ["python", "-m", "pytest", "-m", "not slow and not integration", "-x", "--tb=short"], + [ + "python", + "-m", + "pytest", + "-m", + "not slow and not integration and not performance and not benchmark", + "-x", + "--tb=short", + ], "Fast unit tests", ), # Basic import test diff --git a/scripts/run-all.py b/scripts/run-all.py index f919e90..be4458b 100644 --- a/scripts/run-all.py +++ b/scripts/run-all.py @@ -7,7 +7,8 @@ python scripts/run-all.py # Default: unit tests only python scripts/run-all.py --integration # Include integration tests python scripts/run-all.py --performance # Include performance tests - python scripts/run-all.py --all-tests # Include all tests + python scripts/run-all.py --benchmark # Include benchmark tests + python scripts/run-all.py --all-tests # Include all test types python scripts/run-all.py --ci # CI mode: unit tests only, no cleanup prompts python scripts/run-all.py --help # Show help """ @@ -37,13 +38,20 @@ class LocalRunner: """Local development test runner with cleanup.""" - def __init__(self, include_integration=False, include_performance=False, ci_mode=False): + def __init__( + self, + include_integration=False, + include_performance=False, + include_benchmark=False, + ci_mode=False, + ): self.project_root = Path(__file__).parent.parent self.config = ProjectConfig(self.project_root) self.temp_files = [] self.start_time = time.time() self.include_integration = include_integration self.include_performance = include_performance + self.include_benchmark = include_benchmark self.ci_mode = ci_mode def print_header(self): @@ -61,6 +69,8 @@ def print_header(self): test_modes.append("Integration") if self.include_performance: test_modes.append("Performance") + if self.include_benchmark: + test_modes.append("Benchmark") if not test_modes: test_modes.append("Unit") @@ -207,7 +217,7 @@ def run_all_checks(self) -> bool: ) # Coverage (only for unit tests to avoid NocoDB dependency in CI) - if not self.include_integration: + if not self.include_integration and not self.include_benchmark: checks.append( ( [ @@ -218,7 +228,7 @@ def run_all_checks(self) -> bool: "--cov-report=term-missing", "--cov-report=html", "-m", - "not integration and not performance", + "not integration and not performance and not benchmark", ], "Test coverage (unit tests)", True, @@ -248,9 +258,13 @@ def _build_test_marker(self) -> str: """Build pytest marker expression based on selected test modes.""" markers = [] - if not self.include_integration and not self.include_performance: + if ( + not self.include_integration + and not self.include_performance + and not self.include_benchmark + ): # Default: only unit tests - markers.append("not integration and not performance") + markers.append("not integration and not performance and not benchmark") else: # Build inclusion list included = [] @@ -258,27 +272,35 @@ def _build_test_marker(self) -> str: included.append("integration") if self.include_performance: included.append("performance") + if self.include_benchmark: + included.append("benchmark") # Always include unit tests (tests without markers) if included: markers.append( - f"({' or '.join(included)}) or (not integration and not performance)" + f"({' or '.join(included)}) or (not integration and not performance and not benchmark)" ) else: - markers.append("not integration and not performance") + markers.append("not integration and not performance and not benchmark") return " and ".join(markers) if len(markers) > 1 else markers[0] def _get_test_description(self) -> str: """Get description of which tests are being run.""" - if self.include_integration and self.include_performance: - return "All tests" - elif self.include_integration: - return "Unit + Integration tests" - elif self.include_performance: - return "Unit + Performance tests" - else: + test_types = [] + if self.include_integration: + test_types.append("Integration") + if self.include_performance: + test_types.append("Performance") + if self.include_benchmark: + test_types.append("Benchmark") + + if not test_types: return "Unit tests only" + elif len(test_types) == 3: + return "All test types" + else: + return f"Unit + {' + '.join(test_types)} tests" def _execute_checks(self, checks: list) -> bool: """Execute all checks and return success status.""" @@ -391,6 +413,7 @@ def parse_arguments(): python scripts/run-all.py # Unit tests only (CI safe) python scripts/run-all.py --integration # Include integration tests python scripts/run-all.py --performance # Include performance tests + python scripts/run-all.py --benchmark # Include benchmark tests python scripts/run-all.py --all-tests # Run all test types python scripts/run-all.py --ci # CI mode (unit tests, minimal output) """.strip(), @@ -406,10 +429,12 @@ def parse_arguments(): "--performance", action="store_true", help="Include performance tests (slow)" ) + parser.add_argument("--benchmark", action="store_true", help="Include benchmark tests (slow)") + parser.add_argument( "--all-tests", action="store_true", - help="Run all test types (unit, integration, performance)", + help="Run all test types (unit, integration, performance, benchmark)", ) parser.add_argument( @@ -432,16 +457,19 @@ def main(): # Determine test modes include_integration = args.integration or args.all_tests include_performance = args.performance or args.all_tests + include_benchmark = args.benchmark or args.all_tests ci_mode = args.ci # CI mode overrides - only unit tests in CI if ci_mode: include_integration = False include_performance = False + include_benchmark = False runner = LocalRunner( include_integration=include_integration, include_performance=include_performance, + include_benchmark=include_benchmark, ci_mode=ci_mode, ) From 6059be4bd37c4932265acca91ed02ad5435e971a Mon Sep 17 00:00:00 2001 From: Karl Bauer Date: Fri, 10 Oct 2025 11:37:19 +0200 Subject: [PATCH 2/7] docs: v3 OpenAPI Specifications --- docs/nocodb-openapi-data-v3.json | 7423 ++++++++++++++++++++++++++++++ docs/nocodb-openapi-meta-v3.json | 5567 ++++++++++++++++++++++ 2 files changed, 12990 insertions(+) create mode 100644 docs/nocodb-openapi-data-v3.json create mode 100644 docs/nocodb-openapi-meta-v3.json diff --git a/docs/nocodb-openapi-data-v3.json b/docs/nocodb-openapi-data-v3.json new file mode 100644 index 0000000..9227e2b --- /dev/null +++ b/docs/nocodb-openapi-data-v3.json @@ -0,0 +1,7423 @@ +{ + "openapi": "3.1.0", + "x-stoplight": { + "id": "qiz1rcfqd2jy6" + }, + "info": { + "title": "NocoDB", + "version": null, + "description": "NocoDB API Documentation" + }, + "x-tagGroups": [ + { + "name": "Meta APIs", + "tags": [ + "Bases", + "Tables", + "Views", + "Fields", + "View Filters", + "View Sorts" + ] + }, + { + "name": "Collaboration APIs", + "tags": [ + "Workspace Members", + "Base Members" + ] + } + ], + "servers": [ + { + "url": "https://app.nocodb.com" + } + ], + "paths": { + "/api/v3/meta/workspaces/{workspaceId}/bases": { + "get": { + "summary": "List bases", + "operationId": "bases-list", + "description": "Retrieve a list of bases associated with a specific workspace.", + "tags": [ + "Bases" + ], + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the workspace." + } + ], + "responses": { + "200": { + "description": "The request was successful, and a list of bases is returned.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Base" + }, + "description": "List of bases." + }, + "examples": { + "Example 1": { + "value": { + "list": [ + { + "id": "pgfqcp0ocloo1j3", + "title": "Getting Started", + "meta": { + "icon_color": "#36BFFF" + }, + "created_at": "2025-01-16 06:04:18+00:00", + "updated_at": "2025-01-16 06:04:18+00:00", + "workspace_id": "w6dw3fo0" + }, + { + "id": "p1go4ju5jcwqf8v", + "title": "Base2", + "meta": { + "icon_color": "#36BFFF" + }, + "created_at": "2025-01-16 06:04:48+00:00", + "updated_at": "2025-01-16 06:04:48+00:00", + "workspace_id": "w6dw3fo0" + }, + { + "id": "pho1grz3alkye0t", + "title": "Base3", + "meta": { + "icon_color": "#FA8231" + }, + "created_at": "2025-01-16 06:04:52+00:00", + "updated_at": "2025-01-16 06:04:52+00:00", + "workspace_id": "w6dw3fo0" + } + ] + } + } + } + } + } + }, + "500": { + "description": "The server encountered an unexpected error while processing the request." + } + } + }, + "post": { + "summary": "Create base", + "description": "Create a new base in a specified workspace. The request requires the workspace identifier in the path and base details in the request body.", + "operationId": "base-create", + "tags": [ + "Bases" + ], + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the workspace where the base will be created." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseCreate" + }, + "examples": { + "Example 1": { + "value": { + "title": "New Base", + "meta": { + "icon_color": "#36BFFF" + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Base was created.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Base" + }, + "examples": { + "Example 1": { + "value": { + "id": "p7nwavd2gcdkzvd", + "title": "New Base", + "meta": { + "icon_color": "#36BFFF" + }, + "created_at": "2024-12-28 09:52:29+00:00", + "updated_at": "2024-12-28 09:52:29+00:00", + "workspace_id": "w6dw3fo0" + } + } + } + } + } + }, + "400": { + "description": "Invalid request body." + }, + "500": { + "description": "Server error." + } + } + } + }, + "/api/v3/meta/workspaces/{workspaceId}?include[]=members": { + "get": { + "summary": "List workspace members", + "operationId": "workspace-members-read", + "description": "Retrieve details of a specific workspace, including its members.\n\nNotes:\n- To include member details, use the query parameter `include[]=members`.\n- Workspace collaboration APIs are available only with self-hosted Enterprise plans and cloud-hosted Business+ plans.", + "tags": [ + "Workspace Members" + ], + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the workspace." + }, + { + "name": "include", + "in": "query", + "required": false, + "schema": { + "oneOf": [ + { + "type": "string", + "enum": [ + "members" + ] + }, + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "members" + ] + } + } + ] + }, + "description": "Include additional data. Use 'members' to include workspace member information.", + "example": "members" + } + ], + "responses": { + "200": { + "description": "Workspace metadata retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkspaceWithMembers" + }, + "examples": { + "Workspace with Members": { + "value": { + "id": "w6dw3fo0", + "title": "My Workspace", + "created_at": "2025-01-16T06:04:18.000Z", + "updated_at": "2025-01-16T06:04:18.000Z", + "individual_members": { + "workspace_members": [ + { + "email": "user@example.com", + "user_id": "usrL2PNC5o3H4lBEi", + "created_at": "2025-01-16T06:04:18.000Z", + "updated_at": "2025-01-16T06:04:18.000Z", + "workspace_role": "workspace-level-owner" + } + ] + } + } + } + } + } + } + }, + "404": { + "description": "Workspace not found." + }, + "500": { + "description": "The server encountered an unexpected error while processing the request." + } + } + } + }, + "/api/v3/meta/workspaces/{workspaceId}/members": { + "post": { + "summary": "Add workspace members", + "description": "Add new members to a workspace. The request requires the workspace identifier in the path and member details in the request body.\n\nNotes: Workspace collaboration APIs are available only with self-hosted Enterprise plans and cloud-hosted Business+ plans.", + "operationId": "workspace-members-invite", + "tags": [ + "Workspace Members" + ], + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the workspace where the members will be added." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkspaceUserCreate" + }, + "examples": { + "Example 1": { + "value": [ + { + "email": "user@example.com", + "workspace_role": "workspace-level-viewer" + } + ] + }, + "Example 2": { + "value": [ + { + "email": "user1@example.com", + "workspace_role": "workspace-level-editor" + }, + { + "email": "user2@example.com", + "workspace_role": "workspace-level-viewer" + } + ] + } + } + } + } + }, + "responses": { + "200": { + "description": "Workspace members were added successfully.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkspaceUser" + } + } + } + } + }, + "400": { + "description": "Invalid request body." + }, + "500": { + "description": "Server error." + } + } + }, + "patch": { + "summary": "Update workspace members", + "description": "Update roles of existing workspace members. The request requires the workspace identifier in the path and member update details in the request body.\n\nNotes: Workspace collaboration APIs are available only with self-hosted Enterprise plans and cloud-hosted Business+ plans.", + "operationId": "workspace-members-update", + "tags": [ + "Workspace Members" + ], + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the workspace where the members will be updated." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkspaceUserUpdate" + }, + "examples": { + "Example 1": { + "value": [ + { + "user_id": "user123", + "workspace_role": "workspace-level-editor" + } + ] + }, + "Example 2": { + "value": [ + { + "user_id": "user123", + "workspace_role": "workspace-level-editor" + }, + { + "user_id": "user456", + "workspace_role": "workspace-level-viewer" + } + ] + } + } + } + } + }, + "responses": { + "200": { + "description": "Workspace members were updated successfully.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkspaceUser" + } + } + } + } + }, + "400": { + "description": "Invalid request body." + }, + "500": { + "description": "Server error." + } + } + }, + "delete": { + "summary": "Delete workspace members", + "description": "Remove members from a workspace. The request requires the workspace identifier in the path and member details in the request body.\n\nNotes: Workspace collaboration APIs are available only with self-hosted Enterprise plans and cloud-hosted Business+ plans.", + "operationId": "workspace-members-delete", + "tags": [ + "Workspace Members" + ], + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the workspace where the members will be removed." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkspaceUserDelete" + }, + "examples": { + "Example 1": { + "value": [ + { + "user_id": "user123" + } + ] + }, + "Example 2": { + "value": [ + { + "user_id": "user123" + }, + { + "user_id": "user456" + } + ] + } + } + } + } + }, + "responses": { + "200": { + "description": "Workspace members were removed successfully." + }, + "400": { + "description": "Invalid request body." + }, + "500": { + "description": "Server error." + } + } + } + }, + "/api/v3/meta/bases/{baseId}": { + "get": { + "summary": "Get base meta", + "description": "Retrieve meta details of a specific base using its unique identifier.", + "operationId": "base-read", + "tags": [ + "Bases" + ], + "parameters": [ + { + "name": "baseId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier of the base." + } + ], + "responses": { + "200": { + "description": "Base meta was retrieved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Base" + }, + "examples": { + "Example 1": { + "value": { + "id": "p7nwavd2gcdkzvd", + "title": "Getting Started", + "meta": { + "icon_color": "#36BFFF" + }, + "created_at": "2024-12-28 09:52:29+00:00", + "updated_at": "2024-12-28 09:52:30+00:00", + "workspace_id": "w6dw3fo0", + "sources": [ + { + "id": "biv0qz3hgg191s5", + "title": "jango_fett", + "type": "pg", + "is_schema_readonly": false, + "is_data_readonly": false, + "integration_id": "biv0qz3hgg191s5" + } + ] + } + } + } + } + } + }, + "404": { + "description": "Base not found." + }, + "500": { + "description": "Server error." + } + } + }, + "patch": { + "summary": "Update base", + "description": "Update properties of a specific base. You can modify fields such as the title and metadata of the base. The baseId parameter identifies the base to be updated, and the new details must be provided in the request body. At least one of title or meta must be provided.", + "operationId": "base-update", + "parameters": [ + { + "name": "baseId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the base." + } + ], + "tags": [ + "Bases" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseUpdate" + }, + "examples": { + "Example 1": { + "value": { + "title": "Updated Base Title", + "meta": { + "icon_color": "#36BFFF" + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "The base was updated.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Base" + }, + "examples": { + "Example 1": { + "value": { + "id": "p7nwavd2gcdkzvd", + "title": "Updated Base Title", + "meta": { + "icon_color": "#36BFFF" + }, + "created_at": "2024-12-28 09:52:29+00:00", + "updated_at": "2024-12-28 09:52:29+00:00", + "workspace_id": "w6dw3fo0" + } + } + } + } + } + }, + "400": { + "description": "Invalid request body." + }, + "404": { + "description": "Base not found." + }, + "500": { + "description": "Server error." + } + } + }, + "delete": { + "summary": "Delete base", + "description": "Delete a specific base using its unique identifier. Once deleted, the base and its associated data cannot be recovered.", + "operationId": "base-delete", + "tags": [ + "Bases" + ], + "parameters": [ + { + "name": "baseId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the base." + } + ], + "responses": { + "204": { + "description": "Base was deleted." + }, + "404": { + "description": "Base not found." + }, + "500": { + "description": "Server error." + } + } + } + }, + "/api/v3/meta/bases/{base_id}/tables": { + "get": { + "summary": "List tables", + "description": "Retrieve list of all tables within the specified base.", + "operationId": "tables-list", + "tags": [ + "Tables" + ], + "parameters": [ + { + "name": "base_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the base." + } + ], + "responses": { + "200": { + "description": "List of tables retrieved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TableList" + }, + "examples": { + "Example 1": { + "value": { + "list": [ + { + "id": "mq31p5ngbwj5o7u", + "title": "Features", + "description": "Sample table description.", + "base_id": "pgfqcp0ocloo1j3", + "workspace_id": "w6dw3fo0" + }, + { + "id": "mwmlsgaek7932m4", + "title": "Table-1", + "meta": { + "icon": "🚞" + }, + "base_id": "pgfqcp0ocloo1j3", + "workspace_id": "w6dw3fo0" + }, + { + "id": "m56zsedzrpz6p9w", + "title": "Table-2", + "meta": { + "icon": "🏞️" + }, + "base_id": "pgfqcp0ocloo1j3", + "workspace_id": "w6dw3fo0" + } + ] + } + } + } + } + } + }, + "404": { + "description": "Base not found." + }, + "500": { + "description": "Server error." + } + } + }, + "post": { + "summary": "Create table", + "description": "Create a new table within the specified base by providing the required table details in the request body.", + "operationId": "table-create", + "parameters": [ + { + "name": "base_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the base." + } + ], + "tags": [ + "Tables" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TableCreate" + }, + "examples": { + "Example 1": { + "value": { + "title": "Sample Table20", + "description": "Sample Description", + "fields": [ + { + "title": "SingleLineText", + "type": "SingleLineText", + "default_value": "Default Title" + }, + { + "title": "LongText", + "type": "LongText", + "default_value": "Default Description" + }, + { + "title": "RichText", + "type": "LongText", + "default_value": "**Default Description**", + "options": { + "rich_text": true + } + }, + { + "title": "Email", + "type": "Email", + "default_value": "user@nocodb.com", + "options": { + "validation": true + } + }, + { + "title": "URL", + "type": "URL", + "default_value": "https://nocodb.com", + "options": { + "validation": true + } + }, + { + "title": "Phone", + "type": "PhoneNumber", + "default_value": "1234567890", + "options": { + "validation": true + } + }, + { + "title": "Number", + "type": "Number", + "default_value": "34", + "options": { + "locale_string": true + } + }, + { + "title": "Decimal", + "type": "Decimal", + "default_value": "34.56", + "options": { + "precision": 2 + } + }, + { + "title": "Currency", + "type": "Currency", + "default_value": "34.56", + "options": { + "locale": "en-US", + "code": "USD" + } + }, + { + "title": "Percent", + "type": "Percent", + "default_value": "0.34", + "options": { + "show_as_progress": true + } + }, + { + "title": "Date", + "type": "Date", + "default_value": "2021-09-30", + "options": { + "date_format": "YYYY-MM-DD" + } + }, + { + "title": "DateTime", + "type": "DateTime", + "default_value": "2021-09-30 12:34:56", + "options": { + "date_format": "YYYY-MM-DD HH:mm:ss", + "time_format": "HH:mm:ss", + "12hr_format": true + } + }, + { + "title": "SingleSelect", + "type": "SingleSelect", + "default_value": "Option 1", + "options": { + "choices": [ + { + "title": "Option 1", + "color": "#36BFFF" + }, + { + "title": "Option 2", + "color": "#36BFFF" + } + ] + } + }, + { + "title": "MultiSelect", + "type": "MultiSelect", + "default_value": "[\"Option 1\"]", + "options": { + "choices": [ + { + "title": "Option 1", + "color": "#36BFFF" + }, + { + "title": "Option 2", + "color": "#36BFFF" + } + ] + } + }, + { + "title": "Checkbox", + "type": "Checkbox", + "default_value": true, + "options": { + "icon": "heart", + "color": "#36BFFF" + } + }, + { + "title": "Rating", + "type": "Rating", + "default_value": "3", + "options": { + "icon": "heart", + "max_value": 5, + "color": "#36BFFF" + } + }, + { + "title": "Attachment", + "type": "Attachment" + }, + { + "title": "JSON", + "type": "JSON", + "default_value": "{\"key\": \"value\"}" + }, + { + "title": "User", + "type": "User", + "default_value": "uskfsbdfved8c03z", + "options": { + "allow_multiple_users": true + } + }, + { + "title": "CreatedTime", + "type": "CreatedTime" + }, + { + "title": "CreatedBy", + "type": "CreatedBy" + }, + { + "title": "UpdatedBy", + "type": "LastModifiedBy" + }, + { + "title": "UpdatedTime", + "type": "LastModifiedTime" + } + ] + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Table created.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Table" + }, + "examples": { + "Example 1": { + "value": { + "id": "mo0tpvd1hjvfbrx", + "title": "Sample Table", + "description": "Sample Description", + "source_id": "bzyr9k9ui4oudwx", + "base_id": "p13yxf5plzg73jm", + "workspace_id": "wsdkr9ub", + "display_field_id": "c455mde7px2yidt", + "fields": [ + { + "id": "c455mde7px2yidt", + "title": "SingleLineText", + "type": "SingleLineText", + "default_value": "Default Title" + }, + { + "id": "c9zh8dmi7710mao", + "title": "LongText", + "type": "LongText", + "default_value": "Default Description" + }, + { + "id": "c57o8n0cbyss84l", + "title": "RichText", + "type": "LongText", + "default_value": "**Default Description**", + "options": { + "rich_text": true + } + }, + { + "id": "cnl62kaut6jp5rg", + "title": "Email", + "type": "Email", + "default_value": "user@nocodb.com", + "options": { + "validation": true + } + }, + { + "id": "cx4mpq7zm4offr8", + "title": "URL", + "type": "URL", + "default_value": "https://nocodb.com", + "options": { + "validation": true + } + }, + { + "id": "cx7i90tmjjbh50g", + "title": "Phone", + "type": "PhoneNumber", + "default_value": "1234567890", + "options": { + "validation": true + } + }, + { + "id": "chcb0o99ricceek", + "title": "Number", + "type": "Number", + "default_value": "34", + "options": { + "locale_string": true + } + }, + { + "id": "csumykirz5i02pg", + "title": "Decimal", + "type": "Decimal", + "default_value": "34.56", + "options": { + "precision": 2 + } + }, + { + "id": "cs1tvrdevhwf4r2", + "title": "Currency", + "type": "Currency", + "default_value": "34.56", + "options": { + "locale": "en-US", + "code": "USD" + } + }, + { + "id": "crpqkxsq8d1ljgz", + "title": "Percent", + "type": "Percent", + "default_value": "0.34", + "options": { + "show_as_progress": true + } + }, + { + "id": "crnzx3dtlnjjr5z", + "title": "Date", + "type": "Date", + "default_value": "2021-09-30", + "options": { + "date_format": "YYYY-MM-DD" + } + }, + { + "id": "c26ypextdeug3qd", + "title": "DateTime", + "type": "DateTime", + "default_value": "2021-09-30 12:34:56", + "options": { + "date_format": "YYYY-MM-DD HH:mm:ss", + "time_format": "HH:mm:ss", + "12hr_format": true + } + }, + { + "id": "cyllf65uowcvw1b", + "title": "SingleSelect", + "type": "SingleSelect", + "default_value": "Option 1", + "options": { + "choices": [ + { + "title": "Option 1", + "color": "#36BFFF", + "id": "spd7exnmjz5uch8" + }, + { + "title": "Option 2", + "color": "#36BFFF", + "id": "sytp5rjfte78p8k" + } + ] + } + }, + { + "id": "clhlbmjw6dpz9ts", + "title": "MultiSelect", + "type": "MultiSelect", + "default_value": "[\"Option 1\"]", + "options": { + "choices": [ + { + "title": "Option 1", + "color": "#36BFFF", + "id": "ssig2048lvr9puw" + }, + { + "title": "Option 2", + "color": "#36BFFF", + "id": "sz7etgbnhm84e9w" + } + ] + } + }, + { + "id": "c86vpwa0qzexf8i", + "title": "Checkbox", + "type": "Checkbox", + "default_value": "true", + "options": { + "color": "#36BFFF", + "icon": "heart" + } + }, + { + "id": "c6h2p5nmlmynbiv", + "title": "Rating", + "type": "Rating", + "default_value": "3", + "options": { + "max_value": 5, + "color": "#36BFFF", + "icon": "heart" + } + }, + { + "id": "cqh7kupxqaw7itc", + "title": "Attachment", + "type": "Attachment" + }, + { + "id": "cp1d7w6surl8juh", + "title": "JSON", + "type": "JSON", + "default_value": "{\"key\": \"value\"}" + }, + { + "id": "cebdv0v3uy7tyiy", + "title": "User", + "type": "User", + "default_value": "uskfsbdfved8c03z", + "options": { + "allow_multiple_users": true + } + }, + { + "id": "cb4vptiuveysb5a", + "title": "Links", + "type": "Links", + "options": { + "relation_type": "mm", + "related_table_id": "mnkgbmqwkls36fg" + } + }, + { + "id": "cavokt1gde5dy2m", + "title": "id", + "type": "ID" + }, + { + "id": "csiwamkqrm08slv", + "title": "CreatedTime", + "type": "CreatedTime" + }, + { + "id": "cczd61fh2m0mn8a", + "title": "CreatedBy", + "type": "CreatedBy" + }, + { + "id": "chshjlwe59glff7", + "title": "UpdatedBy", + "type": "LastModifiedBy" + }, + { + "id": "ctlyu6sbs77whvj", + "title": "UpdatedTime", + "type": "LastModifiedTime" + } + ], + "views": [ + { + "id": "vwuke35pn7bcoqmb", + "title": "Sample Table", + "view_type": "grid" + } + ] + } + } + } + } + } + }, + "400": { + "description": "Invalid request body." + }, + "500": { + "description": "Server error." + } + } + } + }, + "/api/v3/meta/bases/{baseId}/tables/{tableId}": { + "get": { + "summary": "Get table schema", + "description": "Retrieve the details of a specific table.", + "operationId": "table-read", + "tags": [ + "Tables" + ], + "parameters": [ + { + "name": "tableId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the table." + }, + { + "name": "baseId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the base." + } + ], + "responses": { + "200": { + "description": "Table details are retrieved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Table" + }, + "examples": { + "Example 1": { + "value": { + "id": "mo0tpvd1hjvfbrx", + "title": "Sample Table", + "description": "Sample Description", + "source_id": "bzyr9k9ui4oudwx", + "base_id": "p13yxf5plzg73jm", + "workspace_id": "wsdkr9ub", + "display_field_id": "c455mde7px2yidt", + "fields": [ + { + "id": "c455mde7px2yidt", + "title": "SingleLineText", + "type": "SingleLineText", + "default_value": "Default Title" + }, + { + "id": "c9zh8dmi7710mao", + "title": "LongText", + "type": "LongText", + "default_value": "Default Description" + }, + { + "id": "c57o8n0cbyss84l", + "title": "RichText", + "type": "LongText", + "default_value": "**Default Description**", + "options": { + "rich_text": true + } + }, + { + "id": "cnl62kaut6jp5rg", + "title": "Email", + "type": "Email", + "default_value": "user@nocodb.com", + "options": { + "validation": true + } + }, + { + "id": "cx4mpq7zm4offr8", + "title": "URL", + "type": "URL", + "default_value": "https://nocodb.com", + "options": { + "validation": true + } + }, + { + "id": "cx7i90tmjjbh50g", + "title": "Phone", + "type": "PhoneNumber", + "default_value": "1234567890", + "options": { + "validation": true + } + }, + { + "id": "chcb0o99ricceek", + "title": "Number", + "type": "Number", + "default_value": "34", + "options": { + "locale_string": true + } + }, + { + "id": "csumykirz5i02pg", + "title": "Decimal", + "type": "Decimal", + "default_value": "34.56", + "options": { + "precision": 2 + } + }, + { + "id": "cs1tvrdevhwf4r2", + "title": "Currency", + "type": "Currency", + "default_value": "34.56", + "options": { + "locale": "en-US", + "code": "USD" + } + }, + { + "id": "crpqkxsq8d1ljgz", + "title": "Percent", + "type": "Percent", + "default_value": "0.34", + "options": { + "show_as_progress": true + } + }, + { + "id": "crnzx3dtlnjjr5z", + "title": "Date", + "type": "Date", + "default_value": "2021-09-30", + "options": { + "date_format": "YYYY-MM-DD" + } + }, + { + "id": "c26ypextdeug3qd", + "title": "DateTime", + "type": "DateTime", + "default_value": "2021-09-30 12:34:56", + "options": { + "date_format": "YYYY-MM-DD HH:mm:ss", + "time_format": "HH:mm:ss", + "12hr_format": true + } + }, + { + "id": "cyllf65uowcvw1b", + "title": "SingleSelect", + "type": "SingleSelect", + "default_value": "Option 1", + "options": { + "choices": [ + { + "title": "Option 1", + "color": "#36BFFF", + "id": "spd7exnmjz5uch8" + }, + { + "title": "Option 2", + "color": "#36BFFF", + "id": "sytp5rjfte78p8k" + } + ] + } + }, + { + "id": "clhlbmjw6dpz9ts", + "title": "MultiSelect", + "type": "MultiSelect", + "default_value": "[\"Option 1\"]", + "options": { + "choices": [ + { + "title": "Option 1", + "color": "#36BFFF", + "id": "ssig2048lvr9puw" + }, + { + "title": "Option 2", + "color": "#36BFFF", + "id": "sz7etgbnhm84e9w" + } + ] + } + }, + { + "id": "c86vpwa0qzexf8i", + "title": "Checkbox", + "type": "Checkbox", + "default_value": "true", + "options": { + "color": "#36BFFF", + "icon": "heart" + } + }, + { + "id": "c6h2p5nmlmynbiv", + "title": "Rating", + "type": "Rating", + "default_value": "3", + "options": { + "max_value": 5, + "color": "#36BFFF", + "icon": "heart" + } + }, + { + "id": "cqh7kupxqaw7itc", + "title": "Attachment", + "type": "Attachment" + }, + { + "id": "cp1d7w6surl8juh", + "title": "JSON", + "type": "JSON", + "default_value": "{\"key\": \"value\"}" + }, + { + "id": "cebdv0v3uy7tyiy", + "title": "User", + "type": "User", + "default_value": "uskfsbdfved8c03z", + "options": { + "allow_multiple_users": true + } + }, + { + "id": "cb4vptiuveysb5a", + "title": "Links", + "type": "Links", + "options": { + "relation_type": "mm", + "related_table_id": "mnkgbmqwkls36fg" + } + }, + { + "id": "cavokt1gde5dy2m", + "title": "id", + "type": "ID" + }, + { + "id": "csiwamkqrm08slv", + "title": "CreatedTime", + "type": "CreatedTime" + }, + { + "id": "cczd61fh2m0mn8a", + "title": "CreatedBy", + "type": "CreatedBy" + }, + { + "id": "chshjlwe59glff7", + "title": "UpdatedBy", + "type": "LastModifiedBy" + }, + { + "id": "ctlyu6sbs77whvj", + "title": "UpdatedTime", + "type": "LastModifiedTime" + } + ], + "views": [ + { + "id": "vwuke35pn7bcoqmb", + "title": "Sample Table", + "view_type": "grid" + } + ] + } + } + } + } + } + }, + "404": { + "description": "Table not found." + }, + "500": { + "description": "Server error." + } + } + }, + "patch": { + "summary": "Update table", + "description": "Update the details of a specific table.", + "operationId": "table-update", + "tags": [ + "Tables" + ], + "parameters": [ + { + "name": "tableId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the table." + }, + { + "name": "baseId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the base." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TableUpdate" + }, + "examples": { + "Example 1": { + "value": { + "title": "Updated Table Title", + "description": "Updated Description", + "display_field_id": "c455mde7px2yidt", + "meta": { + "icon": "🚞" + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Table meta updated.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Table" + }, + "examples": { + "Example 1": { + "value": { + "id": "m0nibecfkcfsz2x", + "title": "Features", + "meta": { + "icon": "🚞" + }, + "source_id": "bbtx6pvj7b8yszy", + "base_id": "pn9mmowpquxj60w", + "workspace_id": "w6wqiisd", + "views": [ + { + "id": "vwuke35pn7bcoqmb", + "title": "Features", + "view_type": "grid" + } + ], + "display_field_id": "cb51p6e2r4gpxpv", + "fields": [ + { + "id": "cz0jslmqdvcv68n", + "title": "Id", + "type": "ID" + }, + { + "id": "cb51p6e2r4gpxpv", + "title": "Title", + "type": "SingleLineText" + } + ] + } + } + } + } + } + }, + "400": { + "description": "Invalid request body." + }, + "404": { + "description": "Table not found." + }, + "500": { + "description": "Server error." + } + } + }, + "delete": { + "summary": "Delete table", + "description": "Delete a specific table.", + "operationId": "table-delete", + "tags": [ + "Tables" + ], + "parameters": [ + { + "name": "tableId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the table." + } + ], + "responses": { + "204": { + "description": "Table deleted successfully." + }, + "404": { + "description": "Table not found." + }, + "500": { + "description": "Server error." + } + } + } + }, + "/api/v3/meta/bases/{baseId}/tables/{tableId}/views": { + "get": { + "summary": "List views", + "operationId": "views-list", + "tags": [ + "Views" + ], + "description": "Retrieve a list of all views for a specific table.", + "parameters": [ + { + "name": "baseId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the base." + }, + { + "name": "tableId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the table." + } + ], + "responses": { + "200": { + "description": "List of views retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ViewList" + }, + "examples": { + "Example 1": { + "value": { + "list": [ + { + "id": "vwpwk5v4urgbu85g", + "table_id": "mrc5unwjdov67vr", + "title": "Grid View", + "type": "grid", + "is_default": true, + "lock_type": "collaborative", + "created_at": "2025-06-26 15:02:56+00:00", + "updated_at": "2025-06-26 15:36:25+00:00" + }, + { + "id": "vwfqa5qnmcaxt9yp", + "table_id": "mrc5unwjdov67vr", + "title": "Grid View 2", + "type": "grid", + "lock_type": "collaborative", + "created_at": "2025-06-26 15:40:01+00:00", + "updated_at": "2025-06-26 15:40:48+00:00", + "description": "Grid view with some description", + "created_by": "usq6o3vavwf0twzr" + }, + { + "id": "vwx2x5ietbis99xg", + "table_id": "mrc5unwjdov67vr", + "title": "Grid Group By", + "type": "grid", + "lock_type": "collaborative", + "created_at": "2025-06-26 15:40:41+00:00", + "updated_at": "2025-06-26 15:40:41+00:00", + "created_by": "usq6o3vavwf0twzr" + }, + { + "id": "vwjctk8vdhbhxtd4", + "table_id": "mrc5unwjdov67vr", + "title": "Gallery View", + "type": "gallery", + "lock_type": "collaborative", + "created_at": "2025-06-26 15:45:30+00:00", + "updated_at": "2025-06-26 15:45:30+00:00", + "created_by": "usq6o3vavwf0twzr" + }, + { + "id": "vwnyxj93jfswla29", + "table_id": "mrc5unwjdov67vr", + "title": "Kanban View", + "type": "kanban", + "lock_type": "collaborative", + "created_at": "2025-06-26 15:45:40+00:00", + "updated_at": "2025-06-26 15:45:40+00:00", + "created_by": "usq6o3vavwf0twzr" + }, + { + "id": "vw7hwhx551tbr44z", + "table_id": "mrc5unwjdov67vr", + "title": "Calendar View", + "type": "calendar", + "lock_type": "collaborative", + "created_at": "2025-06-26 15:45:47+00:00", + "updated_at": "2025-06-26 15:45:47+00:00", + "created_by": "usq6o3vavwf0twzr" + } + ] + } + } + } + } + } + }, + "404": { + "description": "Table not found." + }, + "500": { + "description": "Server error." + } + } + }, + "post": { + "summary": "Create view", + "description": "Create a view for table.", + "operationId": "view-create", + "tags": [ + "Views" + ], + "parameters": [ + { + "name": "baseId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the base." + }, + { + "name": "tableId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the table." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ViewCreate" + }, + "examples": { + "Example 1": { + "value": { + "title": "Created View Title", + "type": "grid" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "View meta updated.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/View" + }, + "examples": { + "Example 1": { + "value": { + "id": "vw5rakj00x28vdht", + "table_id": "mrc5unwjdov67vr", + "title": "myFilterView2", + "type": "grid", + "lock_type": "collaborative", + "created_at": "2025-08-13 07:36:03+00:00", + "updated_at": "2025-08-13 07:36:03+00:00", + "created_by": "uspf4n7qlp704qrj", + "fields": [ + { + "field_id": "csd3tnc2t8ccbw2", + "show": true + }, + { + "field_id": "cwy23c7tglso0zp", + "show": true + }, + { + "field_id": "clljtnye0his0wv", + "show": true + }, + { + "field_id": "cmbwwvp0mhyw6uv", + "show": true + }, + { + "field_id": "c1kzoujfb6on9ba", + "show": true + }, + { + "field_id": "c711uewjhfevrjj", + "show": true + }, + { + "field_id": "cjgsfkb3307h4gj", + "show": true + }, + { + "field_id": "ck6lswg0z3h6ase", + "show": true + }, + { + "field_id": "cxlnws1ojnocsr3", + "show": true + } + ], + "options": { + "row_height": "short" + } + } + } + } + } + } + }, + "400": { + "description": "Invalid request body." + }, + "404": { + "description": "Table not found." + }, + "500": { + "description": "Server error." + } + } + } + }, + "/api/v3/meta/bases/{baseId}/tables/{tableId}/fields": { + "post": { + "summary": "Create field", + "description": "Create a new field within the specified table.", + "operationId": "field-create", + "tags": [ + "Fields" + ], + "parameters": [ + { + "name": "tableId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the table." + }, + { + "name": "baseId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the base." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateField" + }, + "examples": { + "SingleLineText / LongText": { + "value": { + "title": "New Field", + "type": "SingleLineText", + "default_value": "Default Value", + "description": "Sample Description" + } + }, + "RichText": { + "value": { + "title": "New Field", + "type": "LongText", + "default_value": "Default Value", + "description": "Sample Description", + "options": { + "rich_text": true + } + } + }, + "Email / URL / PhoneNumber": { + "value": { + "title": "New Field", + "type": "Email", + "default_value": "user@nocodb.com", + "description": "Sample Description", + "options": { + "validation": true + } + } + }, + "Number / Decimal": { + "value": { + "title": "New Field", + "type": "Number", + "default_value": "23", + "description": "Sample Description", + "options": { + "precision": "4" + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Field was created.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateField" + } + } + } + }, + "400": { + "description": "Invalid request body." + }, + "500": { + "description": "Server error." + } + } + } + }, + "/api/v3/meta/bases/{baseId}/fields/{fieldId}": { + "get": { + "summary": "Get field", + "description": "Retrieve the details of a specific field.", + "operationId": "field-read", + "tags": [ + "Fields" + ], + "parameters": [ + { + "name": "baseId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the base." + }, + { + "name": "fieldId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the field being updated." + } + ], + "responses": { + "200": { + "description": "Field details are retrieved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Field" + } + } + } + }, + "404": { + "description": "Field not found." + }, + "500": { + "description": "Server error." + } + } + }, + "patch": { + "summary": "Update field", + "description": "Update the details of a specific field.", + "operationId": "field-update", + "tags": [ + "Fields" + ], + "parameters": [ + { + "name": "baseId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the base." + }, + { + "name": "fieldId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the field being updated." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FieldUpdate" + }, + "examples": { + "SingleLineText / LongText": { + "value": { + "title": "New Field", + "type": "SingleLineText", + "default_value": "Default Value", + "description": "Sample Description" + } + }, + "RichText": { + "value": { + "title": "New Field", + "type": "LongText", + "default_value": "Default Value", + "description": "Sample Description", + "options": { + "rich_text": true + } + } + }, + "Email / URL / PhoneNumber": { + "value": { + "title": "New Field", + "type": "Email", + "default_value": "user@nocodb.com", + "description": "Sample Description", + "options": { + "validation": true + } + } + }, + "Number / Decimal": { + "value": { + "title": "New Field", + "type": "Number", + "default_value": "23", + "description": "Sample Description", + "options": { + "precision": "4" + } + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Field was updated.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateField" + } + } + } + }, + "400": { + "description": "Invalid request body." + }, + "500": { + "description": "Server error." + } + } + }, + "delete": { + "summary": "Delete field", + "description": "Delete a specific field.", + "operationId": "field-delete", + "tags": [ + "Fields" + ], + "parameters": [ + { + "name": "fieldId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the field." + }, + { + "name": "baseId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the base." + } + ], + "responses": { + "204": { + "description": "Field was deleted." + }, + "404": { + "description": "Field not found." + }, + "500": { + "description": "Server error." + } + } + } + }, + "/api/v3/meta/bases/{baseId}/views/{viewId}": { + "get": { + "summary": "Get view schema", + "description": "Retrieve the details of a specific view.", + "operationId": "view-read", + "tags": [ + "Views" + ], + "parameters": [ + { + "name": "baseId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the base." + }, + { + "name": "viewId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the view." + } + ], + "responses": { + "200": { + "description": "View details are retrieved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/View" + }, + "examples": { + "Example 1": { + "value": { + "id": "vw5rakj00x28vdht", + "table_id": "mrc5unwjdov67vr", + "title": "myFilterView2", + "type": "grid", + "lock_type": "collaborative", + "created_at": "2025-08-13 07:36:03+00:00", + "updated_at": "2025-08-13 07:36:03+00:00", + "created_by": "uspf4n7qlp704qrj", + "fields": [ + { + "field_id": "csd3tnc2t8ccbw2", + "show": true + }, + { + "field_id": "cwy23c7tglso0zp", + "show": true + }, + { + "field_id": "clljtnye0his0wv", + "show": true + }, + { + "field_id": "cmbwwvp0mhyw6uv", + "show": true + }, + { + "field_id": "c1kzoujfb6on9ba", + "show": true + }, + { + "field_id": "c711uewjhfevrjj", + "show": true + }, + { + "field_id": "cjgsfkb3307h4gj", + "show": true + }, + { + "field_id": "ck6lswg0z3h6ase", + "show": true + }, + { + "field_id": "cxlnws1ojnocsr3", + "show": true + } + ], + "options": { + "row_height": "short" + } + } + } + } + } + } + }, + "404": { + "description": "View not found." + }, + "500": { + "description": "Server error." + } + } + }, + "patch": { + "summary": "Update view", + "description": "Update the details of a specific view. The request body should contain the fields to be updated. Fields not included in the request body will remain unchanged. Fields included will overwrite the existing values. There is no provision for partial updates of fields, sort or filter using this PATCH request.", + "operationId": "view-update", + "tags": [ + "Views" + ], + "parameters": [ + { + "name": "baseId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the base." + }, + { + "name": "viewId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the view." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ViewUpdate" + }, + "examples": { + "Example 1": { + "value": { + "title": "Updated View Title" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "View meta updated.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/View" + }, + "examples": { + "Example 1": { + "value": { + "id": "vw5rakj00x28vdht", + "table_id": "mrc5unwjdov67vr", + "title": "myFilterView2", + "type": "grid", + "lock_type": "collaborative", + "created_at": "2025-08-13 07:36:03+00:00", + "updated_at": "2025-08-13 07:36:03+00:00", + "created_by": "uspf4n7qlp704qrj", + "fields": [ + { + "field_id": "csd3tnc2t8ccbw2", + "show": true + }, + { + "field_id": "cwy23c7tglso0zp", + "show": true + }, + { + "field_id": "clljtnye0his0wv", + "show": true + }, + { + "field_id": "cmbwwvp0mhyw6uv", + "show": true + }, + { + "field_id": "c1kzoujfb6on9ba", + "show": true + }, + { + "field_id": "c711uewjhfevrjj", + "show": true + }, + { + "field_id": "cjgsfkb3307h4gj", + "show": true + }, + { + "field_id": "ck6lswg0z3h6ase", + "show": true + }, + { + "field_id": "cxlnws1ojnocsr3", + "show": true + } + ], + "options": { + "row_height": "short" + } + } + } + } + } + } + }, + "400": { + "description": "Invalid request body." + }, + "404": { + "description": "Table not found." + }, + "500": { + "description": "Server error." + } + } + }, + "delete": { + "summary": "Delete view", + "description": "Delete a specific view.", + "operationId": "view-delete", + "tags": [ + "Views" + ], + "parameters": [ + { + "name": "baseId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the base." + }, + { + "name": "viewId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the view." + } + ], + "responses": { + "204": { + "description": "View deleted successfully." + }, + "404": { + "description": "View not found." + }, + "500": { + "description": "Server error." + } + } + } + }, + "/api/v3/meta/bases/{baseId}/views/{viewId}/filters": { + "get": { + "summary": "List view filters", + "description": "Retrieve a list of all filters and groups for a specific view.", + "operationId": "view-filters-list", + "tags": [ + "View Filters" + ], + "parameters": [ + { + "name": "baseId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the base." + }, + { + "name": "viewId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the view." + } + ], + "responses": { + "200": { + "description": "List of filters and groups retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FilterListResponse" + }, + "examples": { + "Example 1": { + "value": { + "list": [ + { + "id": "root", + "group_operator": "AND", + "filters": [ + { + "id": "fiml6n8qzam4wmvl", + "field_id": "c580jyan91qwrbx", + "operator": "eq", + "value": "Midmarket" + }, + { + "id": "fi3egizdiubzfo9u", + "group_operator": "OR", + "filters": [ + { + "id": "fih624x0srskaf1i", + "field_id": "caymvuj3az60wyk", + "operator": "eq", + "value": "Mexico" + }, + { + "id": "fi1pjkf0f88r8s6l", + "field_id": "caymvuj3az60wyk", + "operator": "eq", + "value": "Canada" + } + ] + } + ] + } + ] + } + } + } + } + } + }, + "404": { + "description": "View not found." + }, + "500": { + "description": "Server error." + } + } + }, + "post": { + "summary": "Create filter", + "description": "Create a new filter or filter-group for a specific view.", + "operationId": "view-filter-create", + "tags": [ + "View Filters" + ], + "parameters": [ + { + "name": "baseId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the base." + }, + { + "name": "viewId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the view." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FilterCreateUpdate" + } + } + } + }, + "responses": { + "201": { + "description": "Filter or group created successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FilterCreateUpdate" + } + } + } + }, + "400": { + "description": "Invalid request body." + }, + "500": { + "description": "Server error." + } + } + }, + "patch": { + "summary": "Update filter", + "description": "Update the details of an existing filter or group.", + "operationId": "view-filter-update", + "tags": [ + "View Filters" + ], + "parameters": [ + { + "name": "baseId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the base." + }, + { + "name": "filterId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the filter or group." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FilterCreateUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Filter or group updated successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FilterCreateUpdate" + } + } + } + }, + "400": { + "description": "Invalid request body." + }, + "404": { + "description": "Filter or group not found." + }, + "500": { + "description": "Server error." + } + } + }, + "delete": { + "summary": "Delete filter", + "description": "Delete an existing filter or filter-group.", + "operationId": "view-filter-delete", + "tags": [ + "View Filters" + ], + "parameters": [ + { + "name": "baseId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the base." + }, + { + "name": "viewId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the filter or group." + } + ], + "responses": { + "204": { + "description": "Filter or group deleted successfully." + }, + "404": { + "description": "Filter or group not found." + }, + "500": { + "description": "Server error." + } + } + }, + "put": { + "summary": "Replace filter", + "description": "Replace filters for a specific view. All the existing filters will be overwritten with the new filters specified in this request.", + "operationId": "view-filter-replace", + "tags": [ + "View Filters" + ], + "parameters": [ + { + "name": "viewId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the view. It overwrites all existing filters." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FilterCreateUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Filter or group updated successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FilterCreateUpdate" + } + } + } + }, + "400": { + "description": "Invalid request body." + }, + "404": { + "description": "Filter or group not found." + }, + "500": { + "description": "Server error." + } + } + } + }, + "/api/v3/meta/bases/{baseId}/views/{viewId}/sorts": { + "get": { + "summary": "List view sorts", + "operationId": "view-sorts-list", + "tags": [ + "View Sorts" + ], + "description": "Retrieve a list of all sorts for a specific view.", + "parameters": [ + { + "name": "baseId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the base." + }, + { + "name": "viewId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the view." + } + ], + "responses": { + "200": { + "description": "List of sorts retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SortListResponse" + }, + "examples": { + "Example 1": { + "value": { + "list": [ + { + "id": "so3o7c6po0tktghc", + "field_id": "c580jyan91qwrbx", + "order": "asc" + }, + { + "id": "so1n4x4w7dou8p0v", + "field_id": "caymvuj3az60wyk", + "order": "desc" + } + ] + } + } + } + } + } + }, + "404": { + "description": "View not found." + }, + "500": { + "description": "Server error." + } + } + }, + "post": { + "summary": "Add sort", + "operationId": "view-sort-create", + "description": "Create a new sort for a specific view.", + "tags": [ + "View Sorts" + ], + "parameters": [ + { + "name": "baseId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the base." + }, + { + "name": "viewId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the view." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SortCreate" + }, + "examples": { + "Example 1": { + "value": { + "field_id": "c580jyan91qwrbx", + "order": "asc" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Sort created successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Sort" + } + } + } + }, + "400": { + "description": "Invalid request body." + }, + "500": { + "description": "Server error." + } + } + }, + "delete": { + "summary": "Delete sort", + "description": "Delete an existing sort.", + "operationId": "view-sort-delete", + "tags": [ + "View Sorts" + ], + "parameters": [ + { + "name": "baseId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the base." + }, + { + "name": "sortId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the sort." + } + ], + "responses": { + "204": { + "description": "Sort deleted successfully." + }, + "404": { + "description": "Sort not found." + }, + "500": { + "description": "Server error." + } + } + }, + "patch": { + "summary": "Update sort", + "description": "Update the details of an existing sort.", + "operationId": "view-sort-update", + "tags": [ + "View Sorts" + ], + "parameters": [ + { + "name": "baseId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the base." + }, + { + "name": "sortId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the sort." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SortUpdate" + }, + "examples": { + "Example 1": { + "value": { + "id": "so3o7c6po0tktghc", + "order": "desc" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Sort updated successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Sort" + } + } + } + }, + "400": { + "description": "Invalid request body." + }, + "404": { + "description": "Sort not found." + }, + "500": { + "description": "Server error." + } + } + } + }, + "/api/v3/meta/bases/{baseId}?include[]=members": { + "get": { + "summary": "List base members", + "description": "Retrieve all details of a specific base, including its members.\n\nNotes: Base collaboration APIs are available only with self-hosted Enterprise plans and cloud-hosted Business+ plans.", + "operationId": "base-members-list", + "tags": [ + "Base Members" + ], + "parameters": [ + { + "name": "baseId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier of the base." + } + ], + "responses": { + "200": { + "description": "Base meta with members & their access information was retrieved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseWithMembers" + }, + "examples": { + "Example 1": { + "value": { + "id": "p7nwavd2gcdkzvd", + "title": "Getting Started", + "meta": { + "icon_color": "#36BFFF" + }, + "created_at": "2024-12-28 09:52:29+00:00", + "updated_at": "2024-12-28 09:52:30+00:00", + "workspace_id": "w6dw3fo0", + "individual_members": { + "base_members": [ + { + "email": "user@nocodb.com", + "user_id": "usobpjptvj3m5nu3", + "created_at": "2025-08-12 11:17:47+00:00", + "updated_at": "2025-08-12 11:17:47+00:00", + "base_role": "owner", + "workspace_role": "workspace-level-owner" + } + ], + "workspace_members": [ + { + "user_id": "ussbqtx9b2qjejht", + "created_at": "2025-08-12 11:18:27+00:00", + "updated_at": "2025-08-12 11:18:27+00:00", + "email": "editor@nocodb.com", + "display_name": "", + "workspace_role": "workspace-level-editor" + } + ] + } + } + } + } + } + } + }, + "404": { + "description": "Base not found." + }, + "500": { + "description": "Server error." + } + } + } + }, + "/api/v3/meta/bases/{base_id}/members": { + "post": { + "summary": "Invite base members", + "description": "Invite new members to a specific base using their User ID or Email address.\n\nNotes: Base collaboration APIs are available only with self-hosted Enterprise plans and cloud-hosted Business+ plans.", + "operationId": "base-members-invite", + "parameters": [ + { + "name": "base_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the base." + } + ], + "tags": [ + "Base Members" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseMemberCreate" + } + } + } + }, + "responses": { + "200": { + "description": "Users invited successfully.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseMember" + }, + "description": "List of successfully invited users." + } + } + } + }, + "400": { + "description": "Invalid request body or parameters." + }, + "404": { + "description": "Base not found." + }, + "500": { + "description": "Server error." + } + } + }, + "patch": { + "summary": "Update base members", + "description": "Update roles for existing members in a base.\n\nNotes: Base collaboration APIs are available only with self-hosted Enterprise plans and cloud-hosted Business+ plans.", + "operationId": "base-members-update", + "parameters": [ + { + "name": "base_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the base." + } + ], + "tags": [ + "Base Members" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseMemberUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Users updated successfully.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseMember" + }, + "description": "List of successfully updated users." + } + } + } + }, + "400": { + "description": "Invalid request body or parameters." + }, + "404": { + "description": "Base not found." + }, + "500": { + "description": "Server error." + } + } + }, + "delete": { + "summary": "Delete base members", + "description": "Remove members from a specific base using their IDs.\n\nNotes: Base collaboration APIs are available only with self-hosted Enterprise plans and cloud-hosted Business+ plans.", + "operationId": "base-members-delete", + "parameters": [ + { + "name": "base_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unique identifier for the base." + } + ], + "tags": [ + "Base Members" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BaseMemberDelete" + } + } + } + }, + "responses": { + "200": { + "description": "Users deleted successfully." + }, + "400": { + "description": "Invalid request body or parameters." + }, + "404": { + "description": "Base not found." + }, + "500": { + "description": "Server error." + } + } + } + } + }, + "components": { + "schemas": { + "0": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "formula" + ], + "description": "Button type: formula" + }, + "formula": { + "type": "string", + "description": "Formula to execute" + } + }, + "required": [ + "type", + "formula" + ] + }, + "1": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "webhook" + ], + "description": "Button type: webhook" + }, + "button_hook_id": { + "type": "string", + "description": "ID of the webhook to trigger" + } + }, + "required": [ + "type", + "button_hook_id" + ] + }, + "2": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ai" + ], + "description": "Button type: AI" + }, + "prompt": { + "type": "string", + "description": "AI prompt to execute" + }, + "integration_id": { + "type": "string", + "description": "Integration ID for AI service" + }, + "theme": { + "type": "string", + "description": "Theme of the button" + }, + "output_column_ids": { + "type": "string", + "description": "IDs of columns where AI output should be stored" + }, + "label": { + "type": "string", + "description": "Label of the button" + }, + "icon": { + "type": "string", + "description": "Icon of the button" + }, + "color": { + "type": "string", + "description": "Color of the button" + } + }, + "required": [ + "type", + "prompt", + "integration_id" + ] + }, + "Base": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the base." + }, + "title": { + "type": "string", + "description": "Title of the base." + }, + "meta": { + "$ref": "#/components/schemas/BaseMetaRes" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp of when the base was created." + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp of when the base was last updated." + }, + "workspace_id": { + "type": "string", + "description": "Unique identifier for the workspace to which this base belongs to." + }, + "sources": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the data source." + }, + "title": { + "type": "string", + "description": "Title of the data source." + }, + "type": { + "type": "string", + "description": "Type of the data source (e.g., pg, mysql)." + }, + "is_schema_readonly": { + "type": "boolean", + "description": "Indicates if the schema in this data source is read-only." + }, + "is_data_readonly": { + "type": "boolean", + "description": "Indicates if the data (records) in this data source is read-only." + }, + "integration_id": { + "type": "string", + "description": "Integration ID for the data source." + } + }, + "required": [ + "id", + "title", + "type", + "is_schema_readonly", + "is_data_readonly", + "integration_id" + ] + }, + "description": "List of data sources associated with this base. This information will be included only if one or more external data sources are associated with the base." + } + }, + "required": [ + "id", + "title", + "meta", + "created_at", + "updated_at", + "workspace_id" + ] + }, + "BaseWithMembers": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the base." + }, + "title": { + "type": "string", + "description": "Title of the base." + }, + "meta": { + "$ref": "#/components/schemas/BaseMetaRes" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp of when the base was created." + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp of when the base was last updated." + }, + "workspace_id": { + "type": "string", + "description": "Unique identifier for the workspace to which this base belongs to." + }, + "sources": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the data source." + }, + "title": { + "type": "string", + "description": "Title of the data source." + }, + "type": { + "type": "string", + "description": "Type of the data source (e.g., pg, mysql)." + }, + "is_schema_readonly": { + "type": "boolean", + "description": "Indicates if the schema in this data source is read-only." + }, + "is_data_readonly": { + "type": "boolean", + "description": "Indicates if the data (records) in this data source is read-only." + }, + "integration_id": { + "type": "string", + "description": "Integration ID for the data source." + } + }, + "required": [ + "id", + "title", + "type", + "is_schema_readonly", + "is_data_readonly", + "integration_id" + ] + }, + "description": "List of data sources associated with this base. This information will be included only if one or more external data sources are associated with the base." + }, + "individual_members": { + "type": "object", + "properties": { + "base_members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseMemberWithWorkspaceRole" + } + }, + "workspace_members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkspaceMember" + } + } + } + } + }, + "required": [ + "id", + "title", + "meta", + "created_at", + "updated_at", + "workspace_id" + ] + }, + "BaseMetaRes": { + "type": "object", + "properties": { + "icon_color": { + "type": "string", + "description": "Specifies the color of the base icon using a hexadecimal color code (e.g., `#36BFFF`)", + "pattern": "^#[0-9A-Fa-f]{6}$" + } + } + }, + "BaseMetaReq": { + "type": "object", + "properties": { + "icon_color": { + "type": "string", + "description": "Specifies the color of the base icon using a hexadecimal color code (e.g., `#36BFFF`).\n\n**Constraints**:\n- Must be a valid 6-character hexadecimal color code preceded by a `#`.\n- Optional field; defaults to a standard color if not provided.", + "pattern": "^#[0-9A-Fa-f]{6}$" + } + } + }, + "BaseCreate": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Title of the base." + }, + "meta": { + "$ref": "#/components/schemas/BaseMetaReq" + } + }, + "required": [ + "title" + ] + }, + "BaseUpdate": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Title of the base." + }, + "meta": { + "$ref": "#/components/schemas/BaseMetaReq" + } + } + }, + "TableList": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the table." + }, + "title": { + "type": "string", + "description": "Title of the table." + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "Description of the table." + }, + "meta": { + "$ref": "#/components/schemas/TableMeta" + }, + "base_id": { + "type": "string", + "description": "Unique identifier for the base to which this table belongs to." + }, + "source_id": { + "type": "string", + "description": "Unique identifier for the data source. This information will be included only if the table is associated with an external data source." + }, + "workspace_id": { + "type": "string", + "description": "Unique identifier for the workspace to which this base belongs to." + } + }, + "required": [ + "id", + "title", + "base_id", + "workspace_id" + ] + } + } + }, + "required": [ + "list" + ] + }, + "TableMeta": { + "type": "object", + "properties": { + "icon": { + "type": "string", + "description": "Icon prefix to the table name that needs to be displayed in-lieu of the default table icon." + } + } + }, + "TableCreate": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Title of the table." + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "Description of the table." + }, + "meta": { + "$ref": "#/components/schemas/TableMeta" + }, + "source_id": { + "type": "string", + "description": "Unique identifier for the data source. Include this information only if the table being created is part of a data source." + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CreateField" + } + } + }, + "required": [ + "title" + ] + }, + "FieldOptions": { + "SingleLineText": { + "title": "SingleLineText", + "properties": { + "type": { + "enum": [ + "SingleLineText" + ] + } + } + }, + "LongText": { + "title": "LongText", + "properties": { + "type": { + "enum": [ + "LongText" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_LongText" + } + } + }, + "PhoneNumber": { + "title": "PhoneNumber", + "properties": { + "type": { + "enum": [ + "PhoneNumber" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_PhoneNumber" + } + } + }, + "URL": { + "title": "URL", + "properties": { + "type": { + "enum": [ + "URL" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_URL" + } + } + }, + "Email": { + "title": "Email", + "properties": { + "type": { + "enum": [ + "Email" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Email" + } + } + }, + "Number": { + "title": "Number", + "properties": { + "type": { + "enum": [ + "Number" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Number" + } + } + }, + "Decimal": { + "title": "Decimal", + "properties": { + "type": { + "enum": [ + "Decimal" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Decimal" + } + } + }, + "Currency": { + "title": "Currency", + "properties": { + "type": { + "enum": [ + "Currency" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Currency" + } + } + }, + "Percent": { + "title": "Percent", + "properties": { + "type": { + "enum": [ + "Percent" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Percent" + } + } + }, + "Duration": { + "title": "Duration", + "properties": { + "type": { + "enum": [ + "Duration" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Duration" + } + } + }, + "Date": { + "title": "Date", + "properties": { + "type": { + "enum": [ + "Date" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Date" + } + } + }, + "DateTime": { + "title": "DateTime", + "properties": { + "type": { + "enum": [ + "DateTime" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_DateTime" + } + } + }, + "Time": { + "title": "Time", + "properties": { + "type": { + "enum": [ + "Time" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Time" + } + } + }, + "SingleSelect": { + "title": "SingleSelect", + "properties": { + "type": { + "enum": [ + "SingleSelect" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Select" + } + } + }, + "MultiSelect": { + "title": "MultiSelect", + "properties": { + "type": { + "enum": [ + "MultiSelect" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Select" + } + } + }, + "Rating": { + "title": "Rating", + "properties": { + "type": { + "enum": [ + "Rating" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Rating" + } + } + }, + "Checkbox": { + "title": "Checkbox", + "properties": { + "type": { + "enum": [ + "Checkbox" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Checkbox" + } + } + }, + "Attachment": { + "title": "Attachment", + "properties": { + "type": { + "enum": [ + "Attachment" + ] + } + } + }, + "Geometry": { + "title": "Geometry", + "properties": { + "type": { + "enum": [ + "Geometry" + ] + } + } + }, + "Links": { + "title": "Links", + "properties": { + "type": { + "enum": [ + "Links" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Links" + } + } + }, + "Lookup": { + "title": "Lookup", + "properties": { + "type": { + "enum": [ + "Lookup" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Lookup" + } + } + }, + "Rollup": { + "title": "Rollup", + "properties": { + "type": { + "enum": [ + "Rollup" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Rollup" + } + } + }, + "Button": { + "title": "Button", + "properties": { + "type": { + "enum": [ + "Button" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Button" + } + } + }, + "Formula": { + "title": "Formula", + "properties": { + "type": { + "enum": [ + "Formula" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Formula" + } + } + }, + "Barcode": { + "title": "Barcode", + "properties": { + "type": { + "enum": [ + "Barcode" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Barcode" + } + } + }, + "Year": { + "title": "Year", + "properties": { + "type": { + "enum": [ + "Year" + ] + } + } + }, + "QrCode": { + "title": "QrCode", + "properties": { + "type": { + "enum": [ + "QrCode" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_QrCode" + } + } + }, + "CreatedTime": { + "title": "CreatedTime", + "properties": { + "type": { + "enum": [ + "CreatedTime" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_DateTime" + } + } + }, + "LastModifiedTime": { + "title": "LastModifiedTime", + "properties": { + "type": { + "enum": [ + "LastModifiedTime" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_DateTime" + } + } + }, + "CreatedBy": { + "title": "CreatedBy", + "properties": { + "type": { + "enum": [ + "CreatedBy" + ] + } + } + }, + "LastModifiedBy": { + "title": "LastModifiedBy", + "properties": { + "type": { + "enum": [ + "LastModifiedBy" + ] + } + } + }, + "LinkToAnotherRecord": { + "title": "LinkToAnotherRecord", + "properties": { + "type": { + "enum": [ + "LinkToAnotherRecord" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_LinkToAnotherRecord" + } + } + }, + "User": { + "title": "User", + "properties": { + "type": { + "enum": [ + "User" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_User" + } + } + }, + "JSON": { + "title": "JSON", + "properties": { + "type": { + "enum": [ + "JSON" + ] + } + } + } + }, + "CreateField": { + "allOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/FieldOptions/SingleLineText" + }, + { + "$ref": "#/components/schemas/FieldOptions/LongText" + }, + { + "$ref": "#/components/schemas/FieldOptions/PhoneNumber" + }, + { + "$ref": "#/components/schemas/FieldOptions/URL" + }, + { + "$ref": "#/components/schemas/FieldOptions/Email" + }, + { + "$ref": "#/components/schemas/FieldOptions/Number" + }, + { + "$ref": "#/components/schemas/FieldOptions/Decimal" + }, + { + "$ref": "#/components/schemas/FieldOptions/Currency" + }, + { + "$ref": "#/components/schemas/FieldOptions/Percent" + }, + { + "$ref": "#/components/schemas/FieldOptions/Duration" + }, + { + "$ref": "#/components/schemas/FieldOptions/Date" + }, + { + "$ref": "#/components/schemas/FieldOptions/DateTime" + }, + { + "$ref": "#/components/schemas/FieldOptions/Time" + }, + { + "$ref": "#/components/schemas/FieldOptions/Year" + }, + { + "$ref": "#/components/schemas/FieldOptions/SingleSelect" + }, + { + "$ref": "#/components/schemas/FieldOptions/MultiSelect" + }, + { + "$ref": "#/components/schemas/FieldOptions/Rating" + }, + { + "$ref": "#/components/schemas/FieldOptions/Checkbox" + }, + { + "$ref": "#/components/schemas/FieldOptions/Attachment" + }, + { + "$ref": "#/components/schemas/FieldOptions/JSON" + }, + { + "$ref": "#/components/schemas/FieldOptions/Geometry" + }, + { + "$ref": "#/components/schemas/FieldOptions/Links" + }, + { + "$ref": "#/components/schemas/FieldOptions/Lookup" + }, + { + "$ref": "#/components/schemas/FieldOptions/Rollup" + }, + { + "$ref": "#/components/schemas/FieldOptions/Button" + }, + { + "$ref": "#/components/schemas/FieldOptions/Formula" + }, + { + "$ref": "#/components/schemas/FieldOptions/Barcode" + }, + { + "$ref": "#/components/schemas/FieldOptions/QrCode" + }, + { + "$ref": "#/components/schemas/FieldOptions/CreatedTime" + }, + { + "$ref": "#/components/schemas/FieldOptions/LastModifiedTime" + }, + { + "$ref": "#/components/schemas/FieldOptions/CreatedBy" + }, + { + "$ref": "#/components/schemas/FieldOptions/LastModifiedBy" + }, + { + "$ref": "#/components/schemas/FieldOptions/LinkToAnotherRecord" + }, + { + "$ref": "#/components/schemas/FieldOptions/User" + } + ] + }, + { + "$ref": "#/components/schemas/FieldBaseCreate" + } + ] + }, + "Table": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the table." + }, + "source_id": { + "type": "string", + "description": "Unique identifier for the data source. This information will be included only if the table is associated with an external data source." + }, + "base_id": { + "type": "string", + "description": "Unique identifier for the base to which this table belongs to." + }, + "title": { + "type": "string", + "description": "Title of the table." + }, + "description": { + "type": "string", + "description": "Description of the table." + }, + "display_field_id": { + "type": "string", + "description": "Unique identifier for the display field of the table. First non system field is set as display field by default." + }, + "workspace_id": { + "type": "string", + "description": "Unique identifier for the workspace to which this base belongs to." + }, + "fields": { + "type": "array", + "description": "List of fields associated with this table.", + "items": { + "$ref": "#/components/schemas/CreateField" + } + }, + "views": { + "type": "array", + "description": "List of views associated with this table.", + "items": { + "$ref": "#/components/schemas/ViewSummary" + } + } + }, + "required": [ + "id", + "title", + "base_id", + "workspace_id", + "display_field_id", + "fields", + "views" + ] + }, + "BaseMember": { + "type": "object", + "properties": { + "user_id": { + "type": "string", + "description": "Unique identifier for the user." + }, + "email": { + "type": "string", + "format": "email", + "description": "Email address of the user." + }, + "user_name": { + "type": "string", + "description": "Display name of the user." + }, + "base_role": { + "$ref": "#/components/schemas/BaseRoles" + } + }, + "required": [ + "user_id", + "email", + "created_at", + "updated_at", + "base_role" + ] + }, + "BaseMemberWithWorkspaceRole": { + "type": "object", + "properties": { + "user_id": { + "type": "string", + "description": "Unique identifier for the user." + }, + "email": { + "type": "string", + "format": "email", + "description": "Email address of the user." + }, + "user_name": { + "type": "string", + "description": "Display name of the user." + }, + "base_role": { + "$ref": "#/components/schemas/BaseRoles" + }, + "workspace_role": { + "$ref": "#/components/schemas/WorkspaceRoles", + "description": "Role assigned to the user in the workspace" + } + }, + "required": [ + "user_id", + "email", + "created_at", + "updated_at", + "base_role" + ] + }, + "BaseUserDeleteRequest": {}, + "BaseMemberList": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseMember" + } + } + }, + "required": [ + "users" + ] + }, + "BaseMemberCreate": { + "type": "array", + "items": { + "type": "object", + "properties": { + "base_role": { + "$ref": "#/components/schemas/BaseRoles" + } + }, + "required": [ + "base_role" + ], + "oneOf": [ + { + "title": "Invite User with ID", + "required": [ + "base_role", + "user_id" + ], + "properties": { + "user_id": { + "type": "string", + "description": "Unique identifier for the user (skip if email is provided)" + }, + "user_name": { + "type": "string", + "description": "Full name of the user." + } + } + }, + { + "title": "Invite User with Email", + "required": [ + "base_role", + "email" + ], + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "Email address of the user (skip if user_id is provided)" + }, + "user_name": { + "type": "string", + "description": "Full name of the user." + } + } + } + ], + "description": "An object representing a new member to be created." + }, + "description": "Array of members to be created." + }, + "BaseMemberUpdate": { + "type": "array", + "items": { + "type": "object", + "properties": { + "user_id": { + "type": "string", + "description": "Unique user identifier for the member." + }, + "base_role": { + "$ref": "#/components/schemas/BaseRoles" + } + }, + "required": [ + "user_id", + "base_role" + ], + "description": "An object representing updates for an existing member." + }, + "description": "Array of member updates." + }, + "BaseMemberDelete": { + "type": "array", + "items": { + "type": "object", + "properties": { + "user_id": { + "type": "string", + "description": "User unique identifier for the member." + } + }, + "required": [ + "user_id" + ] + } + }, + "TableMetaReq": { + "type": "object", + "properties": { + "icon": { + "type": "string", + "description": "Icon prefix to the table name that needs to be displayed in-lieu of the default table icon." + } + } + }, + "TableUpdate": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "New title of the table." + }, + "description": { + "type": "string", + "description": "Description of the table." + }, + "display_field_id": { + "type": "string", + "description": "Unique identifier for the display field of the table. The type of the field should be one of the allowed types for display field." + }, + "meta": { + "$ref": "#/components/schemas/TableMetaReq", + "description": "Icon prefix to the table name that needs to be displayed in-lieu of the default table icon." + } + }, + "oneOf": [ + { + "title": "Rename Table", + "required": [ + "title" + ] + }, + { + "title": "Update Table Description", + "required": [ + "description" + ] + }, + { + "title": "Update Display Field", + "required": [ + "display_field_id" + ] + }, + { + "title": "Update Table Icon", + "required": [ + "meta" + ] + } + ] + }, + "Sort": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the sort.", + "readOnly": true + }, + "field_id": { + "type": "string", + "format": "uuid", + "description": "Identifier for the field being sorted." + }, + "direction": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "description": "Sorting direction, either 'asc' (ascending) or 'desc' (descending)." + } + }, + "required": [ + "id", + "field_id", + "direction" + ] + }, + "SortCreate": { + "type": "object", + "properties": { + "field_id": { + "type": "string", + "description": "Identifier for the field being sorted." + }, + "direction": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "description": "Sorting direction, either 'asc' (ascending) or 'desc' (descending)." + } + }, + "required": [ + "field_id" + ] + }, + "SortUpdate": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the sort." + }, + "field_id": { + "type": "string", + "format": "uuid", + "description": "Identifier for the field being sorted." + }, + "direction": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "description": "Sorting direction, either 'asc' (ascending) or 'desc' (descending)." + } + }, + "required": [ + "id" + ] + }, + "ViewSummary": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "description": "Unique identifier for the view." + }, + "title": { + "type": "string", + "description": "Name of the view." + }, + "view_type": { + "type": "string", + "enum": [ + "grid", + "gallery", + "kanban", + "calendar", + "form" + ], + "description": "Type of the view." + } + } + }, + "ViewAggregationEnum": { + "type": "string", + "enum": [ + "sum", + "min", + "max", + "avg", + "median", + "std_dev", + "range", + "count", + "count_empty", + "count_filled", + "count_unique", + "percent_empty", + "percent_filled", + "percent_unique", + "none", + "attachment_size", + "checked", + "unchecked", + "percent_checked", + "percent_unchecked", + "earliest_date", + "latest_date", + "date_range", + "month_range" + ] + }, + "ViewList": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the view." + }, + "table_id": { + "type": "string", + "description": "Id of table associated with the view." + }, + "title": { + "type": "string", + "description": "Title of the view." + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "Description of the view." + }, + "type": { + "type": "string", + "enum": [ + "grid", + "gallery", + "kanban", + "calendar", + "form" + ], + "description": "Type of the view." + }, + "lock_type": { + "type": "string", + "enum": [ + "collaborative", + "locked", + "personal" + ], + "description": "View configuration edit state." + }, + "is_default": { + "type": "boolean", + "description": "Indicates if this is the default view." + }, + "created_by": { + "type": "string", + "description": "User ID of the creator." + }, + "owned_by": { + "type": "string", + "description": "User ID of the owner. Applicable only for personal views." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp of creation." + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp of last update." + } + }, + "required": [ + "id", + "title", + "type", + "lock_type", + "created_at", + "updated_at", + "created_by" + ] + } + } + }, + "required": [ + "list" + ] + }, + "ViewBase": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Title of the view." + }, + "type": { + "type": "string", + "enum": [ + "grid", + "gallery", + "kanban", + "calendar" + ], + "description": "Type of the view. \n\nNote: Form view via API is not supported currently" + }, + "lock_type": { + "type": "string", + "enum": [ + "collaborative", + "locked", + "personal" + ], + "description": "Lock type of the view.\n\n Note: Assigning view as personal using API is not supported currently", + "default": "collaborative" + }, + "description": { + "type": "string", + "description": "Description of the view." + } + }, + "required": [ + "title", + "type" + ] + }, + "ViewBaseInUpdate": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Title of the view." + }, + "lock_type": { + "type": "string", + "enum": [ + "collaborative", + "locked", + "personal" + ], + "description": "Lock type of the view.\n\n Note: Assigning view as personal using API is not supported currently", + "default": "collaborative" + }, + "description": { + "type": "string", + "description": "Description of the view." + } + } + }, + "ViewFields": { + "type": "array", + "items": { + "type": "object", + "properties": { + "field_id": { + "type": "string", + "description": "Unique identifier for the field." + }, + "show": { + "type": "boolean", + "description": "Indicates whether the field should be displayed in the view." + }, + "width": { + "type": "integer", + "description": "Width of the field in pixels.\n\n **Applicable only for grid view.**" + }, + "aggregation": { + "$ref": "#/components/schemas/ViewAggregationEnum", + "description": "Aggregation function to be applied to the field.\n\n **Applicable only for grid view.**" + } + }, + "required": [ + "field_id", + "show" + ] + }, + "description": "List of fields to be displayed in the view. \n\n- If not specified, all fields are displayed by default.\n- If an empty array is provided, only the display value field will be shown.\n- In case of partial list, fields not included in the list will be excluded from the view." + }, + "ViewRowColour": { + "type": "object", + "oneOf": [ + { + "type": "object", + "title": "conditions", + "properties": { + "mode": { + "type": "string", + "enum": [ + "filter" + ], + "description": "Mode of row coloring. In this mode, the color is selected based on conditions applied to the fields." + }, + "conditions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "apply_as_row_background": { + "type": "boolean" + }, + "color": { + "type": "string" + }, + "filters": { + "$ref": "#/components/schemas/FilterCreateUpdate" + } + } + } + } + }, + "required": [ + "mode", + "conditions" + ] + }, + { + "type": "object", + "title": "select", + "properties": { + "mode": { + "type": "string", + "enum": [ + "select" + ], + "description": "Mode of row coloring. In this mode, the color is selected based on a single select field." + }, + "field_id": { + "type": "string", + "description": "Single select field ID to be used for colouring rows in the view." + }, + "apply_as_row_background": { + "type": "boolean", + "description": "Whether to additionally apply the color as row background." + } + }, + "required": [ + "mode", + "field_id" + ] + } + ], + "discriminator": { + "propertyName": "mode" + } + }, + "ViewOptionsGrid": { + "type": "object", + "properties": { + "groups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "field_id": { + "type": "string", + "description": "Identifier for the field being sorted." + }, + "direction": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "description": "Direction of the group, either 'asc' (ascending) or 'desc' (descending).", + "default": "asc" + } + }, + "required": [ + "field_id" + ] + }, + "description": "List of groups to be applied on the grid view." + }, + "row_height": { + "type": "string", + "enum": [ + "short", + "medium", + "tall", + "extra" + ], + "description": "Height of the rows in the grid view.", + "default": "short" + } + } + }, + "ViewOptionsKanban": { + "type": "object", + "properties": { + "stack_by": { + "type": "object", + "properties": { + "field_id": { + "type": "string", + "description": "Single select field ID to be used for stacking cards in kanban view." + }, + "stack_order": { + "type": "array", + "description": "Order of the stacks in kanban view. If not provided, the order will be determined by options listed in associated field.\n\nExample: ```stack_order: ['option1', 'option2', 'option3']```", + "items": { + "type": "string" + } + } + }, + "required": [ + "field_id" + ] + }, + "cover_field_id": { + "type": "string", + "description": "Attachment field ID to be used as cover image in kanban view. If not provided, cover field configuration is skipped." + } + }, + "required": [ + "stack_by" + ] + }, + "ViewOptionsCalendar": { + "type": "object", + "properties": { + "date_ranges": { + "type": "array", + "items": { + "type": "object", + "properties": { + "start_date_field_id": { + "type": "string", + "description": "Date field ID to be used as start date in calendar view." + }, + "end_date_field_id": { + "type": "string", + "description": "Date field ID to be used as end date in calendar view." + } + }, + "required": [ + "start_date_field_id" + ] + } + } + }, + "required": [ + "date_ranges" + ] + }, + "ViewOptionsGallery": { + "type": "object", + "properties": { + "cover_field_id": { + "type": "string", + "description": "Attachment field ID to be used as cover image in gallery view. Is optional, if not provided, the first attachment field will be used." + } + } + }, + "ViewOptionsForm": { + "type": "object", + "properties": { + "form_title": { + "type": "string", + "description": "Heading for the form." + }, + "form_description": { + "type": "string", + "description": "Subheading for the form." + }, + "thank_you_message": { + "type": "string", + "description": "Success message shown after form submission." + }, + "form_redirect_after_secs": { + "type": "integer", + "description": "Seconds to wait before redirecting." + }, + "show_submit_another_button": { + "type": "boolean", + "description": "Whether to show another form after submission." + }, + "reset_form_after_submit": { + "type": "boolean", + "description": "Whether to show a blank form after submission." + }, + "form_hide_banner": { + "type": "boolean", + "description": "Whether to hide the banner on the form." + }, + "form_hide_branding": { + "type": "boolean", + "description": "Whether to hide branding on the form." + }, + "banner": { + "type": "string", + "format": "uri", + "description": "URL of the banner image for the form." + }, + "logo": { + "type": "string", + "format": "uri", + "description": "URL of the logo for the form." + }, + "form_background_color": { + "type": "string", + "description": "Background color for the form.", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "redirect_url": { + "type": "string", + "format": "uri", + "description": "URL to redirect to after form submission." + } + } + }, + "ViewCreate": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/ViewBase" + }, + { + "oneOf": [ + { + "type": "object", + "title": "grid", + "properties": { + "type": { + "type": "string", + "enum": [ + "grid" + ] + }, + "options": { + "$ref": "#/components/schemas/ViewOptionsGrid" + }, + "sorts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortCreate" + }, + "description": "List of sorts to be applied to the view." + }, + "filters": { + "$ref": "#/components/schemas/FilterCreateUpdate" + }, + "fields": { + "$ref": "#/components/schemas/ViewFields" + }, + "row_coloring": { + "$ref": "#/components/schemas/ViewRowColour", + "description": "Row colour configuration for the the view." + } + } + }, + { + "type": "object", + "title": "gallery", + "properties": { + "type": { + "type": "string", + "enum": [ + "gallery" + ] + }, + "options": { + "$ref": "#/components/schemas/ViewOptionsGallery" + }, + "sorts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortCreate" + }, + "description": "List of sorts to be applied to the view." + }, + "filters": { + "$ref": "#/components/schemas/FilterCreateUpdate" + }, + "fields": { + "$ref": "#/components/schemas/ViewFields" + }, + "row_coloring": { + "$ref": "#/components/schemas/ViewRowColour", + "description": "Row colour configuration for the the view." + } + } + }, + { + "type": "object", + "title": "kanban", + "properties": { + "type": { + "type": "string", + "enum": [ + "kanban" + ] + }, + "options": { + "$ref": "#/components/schemas/ViewOptionsKanban" + }, + "sorts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortCreate" + }, + "description": "List of sorts to be applied to the view." + }, + "filters": { + "$ref": "#/components/schemas/FilterCreateUpdate" + }, + "fields": { + "$ref": "#/components/schemas/ViewFields" + }, + "row_coloring": { + "$ref": "#/components/schemas/ViewRowColour", + "description": "Row colour configuration for the the view." + } + }, + "required": [ + "options" + ] + }, + { + "type": "object", + "title": "calendar", + "properties": { + "type": { + "type": "string", + "enum": [ + "calendar" + ] + }, + "options": { + "$ref": "#/components/schemas/ViewOptionsCalendar" + }, + "sorts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortCreate" + }, + "description": "List of sorts to be applied to the view." + }, + "filters": { + "$ref": "#/components/schemas/FilterCreateUpdate" + }, + "fields": { + "$ref": "#/components/schemas/ViewFields" + }, + "row_coloring": { + "$ref": "#/components/schemas/ViewRowColour", + "description": "Row colour configuration for the the view." + } + }, + "required": [ + "options" + ] + } + ], + "discriminator": { + "propertyName": "type" + } + } + ] + }, + "ViewUpdate": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/ViewBaseInUpdate" + }, + { + "oneOf": [ + { + "type": "object", + "title": "grid", + "properties": { + "options": { + "$ref": "#/components/schemas/ViewOptionsGrid" + }, + "sorts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortCreate" + }, + "description": "List of sorts to be applied to the view." + }, + "filters": { + "$ref": "#/components/schemas/FilterCreateUpdate" + }, + "fields": { + "$ref": "#/components/schemas/ViewFields" + }, + "row_coloring": { + "$ref": "#/components/schemas/ViewRowColour", + "description": "Row colour configuration for the the view." + } + } + }, + { + "type": "object", + "title": "gallery", + "properties": { + "options": { + "$ref": "#/components/schemas/ViewOptionsGallery" + }, + "sorts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortCreate" + }, + "description": "List of sorts to be applied to the view." + }, + "filters": { + "$ref": "#/components/schemas/FilterCreateUpdate" + }, + "fields": { + "$ref": "#/components/schemas/ViewFields" + }, + "row_coloring": { + "$ref": "#/components/schemas/ViewRowColour", + "description": "Row colour configuration for the the view." + } + } + }, + { + "type": "object", + "title": "kanban", + "properties": { + "options": { + "$ref": "#/components/schemas/ViewOptionsKanban" + }, + "sorts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortCreate" + }, + "description": "List of sorts to be applied to the view." + }, + "filters": { + "$ref": "#/components/schemas/FilterCreateUpdate" + }, + "fields": { + "$ref": "#/components/schemas/ViewFields" + }, + "row_coloring": { + "$ref": "#/components/schemas/ViewRowColour", + "description": "Row colour configuration for the the view." + } + } + }, + { + "type": "object", + "title": "calendar", + "properties": { + "options": { + "$ref": "#/components/schemas/ViewOptionsCalendar" + }, + "sorts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortCreate" + }, + "description": "List of sorts to be applied to the view." + }, + "filters": { + "$ref": "#/components/schemas/FilterCreateUpdate" + }, + "fields": { + "$ref": "#/components/schemas/ViewFields" + }, + "row_coloring": { + "$ref": "#/components/schemas/ViewRowColour", + "description": "Row colour configuration for the the view." + } + } + } + ], + "discriminator": { + "propertyName": "type" + } + } + ] + }, + "View": { + "type": "object", + "allOf": [ + { + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the view." + }, + "table_id": { + "type": "string", + "description": "Id of table associated with the view." + }, + "is_default": { + "type": "boolean", + "description": "Indicates if this is the default view. Omitted if not the default view." + } + }, + "required": [ + "id" + ] + }, + { + "$ref": "#/components/schemas/ViewBase" + }, + { + "properties": { + "created_by": { + "type": "string", + "description": "User ID of the creator." + }, + "owned_by": { + "type": "string", + "description": "User ID of the owner." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp of creation." + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp of last update." + } + } + }, + { + "oneOf": [ + { + "type": "object", + "title": "grid", + "properties": { + "type": { + "type": "string", + "enum": [ + "grid" + ] + }, + "options": { + "$ref": "#/components/schemas/ViewOptionsGrid" + }, + "sorts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortCreate" + }, + "description": "List of sorts to be applied to the view." + }, + "filters": { + "$ref": "#/components/schemas/FilterCreateUpdate" + }, + "fields": { + "$ref": "#/components/schemas/ViewFields" + }, + "row_coloring": { + "$ref": "#/components/schemas/ViewRowColour", + "description": "Row colour configuration for the the view." + } + } + }, + { + "type": "object", + "title": "gallery", + "properties": { + "type": { + "type": "string", + "enum": [ + "gallery" + ] + }, + "options": { + "$ref": "#/components/schemas/ViewOptionsGallery" + }, + "sorts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortCreate" + }, + "description": "List of sorts to be applied to the view." + }, + "filters": { + "$ref": "#/components/schemas/FilterCreateUpdate" + }, + "fields": { + "$ref": "#/components/schemas/ViewFields" + }, + "row_coloring": { + "$ref": "#/components/schemas/ViewRowColour", + "description": "Row colour configuration for the the view." + } + } + }, + { + "type": "object", + "title": "kanban", + "properties": { + "type": { + "type": "string", + "enum": [ + "kanban" + ] + }, + "options": { + "$ref": "#/components/schemas/ViewOptionsKanban" + }, + "sorts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortCreate" + }, + "description": "List of sorts to be applied to the view." + }, + "filters": { + "$ref": "#/components/schemas/FilterCreateUpdate" + }, + "fields": { + "$ref": "#/components/schemas/ViewFields" + }, + "row_coloring": { + "$ref": "#/components/schemas/ViewRowColour", + "description": "Row colour configuration for the the view." + } + }, + "required": [ + "options" + ] + }, + { + "type": "object", + "title": "calendar", + "properties": { + "type": { + "type": "string", + "enum": [ + "calendar" + ] + }, + "options": { + "$ref": "#/components/schemas/ViewOptionsCalendar" + }, + "sorts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortCreate" + }, + "description": "List of sorts to be applied to the view." + }, + "filters": { + "$ref": "#/components/schemas/FilterCreateUpdate" + }, + "fields": { + "$ref": "#/components/schemas/ViewFields" + }, + "row_coloring": { + "$ref": "#/components/schemas/ViewRowColour", + "description": "Row colour configuration for the the view." + } + }, + "required": [ + "options" + ] + } + ], + "discriminator": { + "propertyName": "type" + } + } + ] + }, + "FieldBase": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the field.", + "readOnly": true, + "writeOnly": false + }, + "title": { + "type": "string", + "description": "Title of the field." + }, + "type": { + "type": "string", + "enum": [ + "SingleLineText", + "LongText", + "PhoneNumber", + "URL", + "Email", + "Number", + "Decimal", + "Currency", + "Percent", + "Duration", + "Date", + "DateTime", + "Time", + "SingleSelect", + "MultiSelect", + "Rating", + "Checkbox", + "Attachment", + "Geometry", + "Links", + "Lookup", + "Rollup", + "Button", + "Formula", + "Barcode", + "Year", + "QrCode", + "CreatedTime", + "LastModifiedTime", + "CreatedBy", + "LastModifiedBy", + "LinkToAnotherRecord", + "User", + "JSON" + ], + "description": "Field data type." + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "Description of the field." + }, + "default_value": { + "type": [ + "string", + "boolean", + "number" + ], + "description": "Default value for the field. Applicable for SingleLineText, LongText, PhoneNumber, URL, Email, Number, Decimal, Currency, Percent, Duration, Date, DateTime, Time, SingleSelect, MultiSelect, Rating, Checkbox, User and JSON fields." + } + }, + "required": [ + "title" + ] + }, + "FieldBaseCreate": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldBase" + }, + { + "required": [ + "title", + "type" + ] + } + ] + }, + "FieldOptions_LongText": { + "type": "object", + "title": "LongText", + "properties": { + "rich_text": { + "type": "boolean", + "description": "Enable rich text formatting." + }, + "generate_text_using_ai": { + "type": "boolean", + "description": "Enable text generation for this field using NocoAI." + } + }, + "additionalProperties": false + }, + "FieldOptions_PhoneNumber": { + "type": "object", + "title": "PhoneNumber", + "properties": { + "validation": { + "type": "boolean", + "description": "Enable validation for phone numbers." + } + }, + "additionalProperties": false + }, + "FieldOptions_URL": { + "type": "object", + "title": "URL", + "properties": { + "validation": { + "type": "boolean", + "description": "Enable validation for URL." + } + }, + "additionalProperties": false + }, + "FieldOptions_Email": { + "type": "object", + "title": "Email", + "properties": { + "validation": { + "type": "boolean", + "description": "Enable validation for Email." + } + }, + "additionalProperties": false + }, + "FieldOptions_Number": { + "type": "object", + "title": "Number", + "properties": { + "locale_string": { + "type": "boolean", + "description": "Show thousand separator on the UI." + } + }, + "additionalProperties": false + }, + "FieldOptions_Decimal": { + "type": "object", + "title": "Decimal", + "properties": { + "precision": { + "type": "number", + "description": "Decimal field precision. Defaults to 0", + "minimum": 0, + "maximum": 5 + } + }, + "additionalProperties": false + }, + "FieldOptions_Currency": { + "type": "object", + "title": "Currency", + "description": "Currency settings for this column. Locale defaults to `en-US` and currency code defaults to `USD`", + "properties": { + "locale": { + "type": "string", + "description": "Locale for currency formatting. Refer https://simplelocalize.io/data/locales/" + }, + "code": { + "type": "string", + "description": "Currency code. Refer https://simplelocalize.io/data/locales/", + "enum": [ + "AED", + "AFN", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BOV", + "BRL", + "BSD", + "BTN", + "BWP", + "BYR", + "BZD", + "CAD", + "CDF", + "CHE", + "CHF", + "CHW", + "CLF", + "CLP", + "CNY", + "COP", + "COU", + "CRC", + "CUP", + "CVE", + "CYP", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EEK", + "EGP", + "ERN", + "ETB", + "EUR", + "FJD", + "FKP", + "GBP", + "GEL", + "GHC", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HRK", + "HTG", + "HUF", + "IDR", + "ILS", + "INR", + "IQD", + "IRR", + "ISK", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KPW", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LTL", + "LVL", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRO", + "MTL", + "MUR", + "MVR", + "MWK", + "MXN", + "MXV", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "ROL", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SDD", + "SEK", + "SGD", + "SHP", + "SIT", + "SKK", + "SLL", + "SOS", + "SRD", + "STD", + "SYP", + "SZL", + "THB", + "TJS", + "TMM", + "TND", + "TOP", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "USN", + "USS", + "UYU", + "UZS", + "VEB", + "VND", + "VUV", + "WST", + "XAF", + "XAG", + "XAU", + "XBA", + "XBB", + "XBC", + "XBD", + "XCD", + "XDR", + "XFO", + "XFU", + "XOF", + "XPD", + "XPF", + "XPT", + "XTS", + "XXX", + "YER", + "ZAR", + "ZMK", + "ZWD" + ] + } + }, + "additionalProperties": false + }, + "FieldOptions_Percent": { + "type": "object", + "title": "Percent", + "properties": { + "show_as_progress": { + "type": "boolean", + "description": "Display as a progress bar." + } + }, + "additionalProperties": false + }, + "FieldOptions_Duration": { + "type": "object", + "title": "Duration", + "properties": { + "duration_format": { + "type": "string", + "description": "Duration format. Supported options are listed below\n- `h:mm`\n- `h:mm:ss`\n- `h:mm:ss.S`\n- `h:mm:ss.SS`\n- `h:mm:ss.SSS`" + } + }, + "additionalProperties": false + }, + "FieldOptions_DateTime": { + "type": "object", + "title": "DateTime", + "properties": { + "date_format": { + "type": "string", + "description": "Date format. Supported options are listed below\n- `YYYY/MM/DD`\n- `YYYY-MM-DD`\n- `YYYY MM DD`\n- `DD/MM/YYYY`\n- `DD-MM-YYYY`\n- `DD MM YYYY`\n- `MM/DD/YYYY`\n- `MM-DD-YYYY`\n- `MM DD YYYY`\n- `YYYY-MM`\n- `YYYY MM`" + }, + "time_format": { + "type": "string", + "description": "Time format. Supported options are listed below\n- `HH:mm`\n- `HH:mm:ss`\n- `HH:mm:ss.SSS`" + }, + "12hr_format": { + "type": "boolean", + "description": "Use 12-hour time format." + }, + "display_timezone": { + "type": "boolean", + "description": "Display timezone." + }, + "timezone": { + "type": "string", + "description": "Timezone. Refer to https://en.wikipedia.org/wiki/List_of_tz_database_time_zones" + }, + "use_same_timezone_for_all": { + "type": "boolean", + "description": "Use same timezone for all records." + } + }, + "additionalProperties": false + }, + "FieldOptions_Date": { + "type": "object", + "title": "Date", + "properties": { + "date_format": { + "type": "string", + "description": "Date format. Supported options are listed below\n- `YYYY/MM/DD`\n- `YYYY-MM-DD`\n- `YYYY MM DD`\n- `DD/MM/YYYY`\n- `DD-MM-YYYY`\n- `DD MM YYYY`\n- `MM/DD/YYYY`\n- `MM-DD-YYYY`\n- `MM DD YYYY`\n- `YYYY-MM`\n- `YYYY MM`" + } + }, + "additionalProperties": false + }, + "FieldOptions_Time": { + "type": "object", + "title": "Time", + "properties": { + "12hr_format": { + "type": "boolean", + "description": "Use 12-hour time format." + } + }, + "additionalProperties": false + }, + "FieldOptions_Select": { + "type": "object", + "title": "Single & MultiSelect", + "properties": { + "choices": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Choice title." + }, + "color": { + "type": "string", + "description": "Specifies the tile color for the choice using a hexadecimal color code (e.g., `#36BFFF`).", + "pattern": "^#[0-9A-Fa-f]{6}$" + } + }, + "required": [ + "title" + ] + }, + "uniqueItems": true + } + }, + "additionalProperties": false + }, + "FieldOptions_Rating": { + "type": "object", + "title": "Rating", + "properties": { + "icon": { + "type": "string", + "enum": [ + "star", + "heart", + "circle-filled", + "thumbs-up", + "flag" + ], + "description": "Icon to display rating on the UI. Supported options are listed below\n- `star`\n- `heart`\n- `circle-filled`\n- `thumbs-up`\n- `flag`" + }, + "max_value": { + "type": "integer", + "description": "Maximum value for the rating. Allowed range: 1-10.", + "minimum": 1, + "maximum": 10 + }, + "color": { + "type": "string", + "description": "Specifies icon color using a hexadecimal color code (e.g., `#36BFFF`).", + "pattern": "^#[0-9A-Fa-f]{6}$" + } + }, + "additionalProperties": false + }, + "FieldOptions_Checkbox": { + "type": "object", + "title": "Checkbox", + "properties": { + "icon": { + "type": "string", + "enum": [ + "square", + "circle-check", + "circle-filled", + "star", + "heart", + "thumbs-up", + "flag" + ], + "description": "Icon to display checkbox on the UI. Supported options are listed below\n- `square`\n- `circle-check`\n- `circle-filled`\n- `star`\n- `heart`\n- `thumbs-up`\n- `flag`" + }, + "color": { + "type": "string", + "description": "Specifies icon color using a hexadecimal color code (e.g., `#36BFFF`).", + "pattern": "^#[0-9A-Fa-f]{6}$" + } + }, + "additionalProperties": false + }, + "FieldOptions_Barcode": { + "type": "object", + "title": "Barcode", + "properties": { + "format": { + "type": "string", + "description": "Barcode format (e.g., CODE128)." + }, + "barcode_value_field_id": { + "type": "string", + "description": "Field ID that contains the value." + } + }, + "additionalProperties": false + }, + "FieldOptions_QrCode": { + "type": "object", + "title": "QrCode", + "properties": { + "qrcode_value_field_id": { + "type": "string", + "description": "Field ID that contains the value." + } + }, + "additionalProperties": false + }, + "FieldOptions_Formula": { + "type": "object", + "title": "Formula", + "properties": { + "formula": { + "type": "string", + "description": "Formula expression." + } + }, + "additionalProperties": false + }, + "FieldOptions_User": { + "type": "object", + "title": "User", + "properties": { + "allow_multiple_users": { + "type": "boolean", + "description": "Allow selecting multiple users." + } + }, + "additionalProperties": false + }, + "FieldOptions_Lookup": { + "type": "object", + "title": "Lookup", + "properties": { + "related_field_id": { + "type": "string", + "description": "Linked field ID. Can be of type Links or LinkToAnotherRecord" + }, + "related_table_lookup_field_id": { + "type": "string", + "description": "Lookup field ID in the linked table." + } + }, + "required": [ + "related_field_id", + "related_table_lookup_field_id" + ], + "additionalProperties": false + }, + "FieldOptions_Rollup": { + "type": "object", + "title": "Rollup", + "properties": { + "related_field_id": { + "type": "string", + "description": "Linked field ID." + }, + "related_table_rollup_field_id": { + "type": "string", + "description": "Rollup field ID in the linked table." + }, + "rollup_function": { + "type": "string", + "description": "Rollup function.", + "enum": [ + "count", + "min", + "max", + "avg", + "sum", + "countDistinct", + "sumDistinct", + "avgDistinct" + ] + } + }, + "required": [ + "related_field_id", + "related_table_rollup_field_id", + "rollup_function" + ], + "additionalProperties": false + }, + "FieldOptions_Button": { + "type": "object", + "title": "Button", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "formula" + ], + "description": "Button type: formula" + }, + "formula": { + "type": "string", + "description": "Formula to execute" + } + }, + "required": [ + "type", + "formula" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "webhook" + ], + "description": "Button type: webhook" + }, + "button_hook_id": { + "type": "string", + "description": "ID of the webhook to trigger" + } + }, + "required": [ + "type", + "button_hook_id" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ai" + ], + "description": "Button type: AI" + }, + "prompt": { + "type": "string", + "description": "AI prompt to execute" + }, + "integration_id": { + "type": "string", + "description": "Integration ID for AI service" + }, + "theme": { + "type": "string", + "description": "Theme of the button" + }, + "output_column_ids": { + "type": "string", + "description": "IDs of columns where AI output should be stored" + }, + "label": { + "type": "string", + "description": "Label of the button" + }, + "icon": { + "type": "string", + "description": "Icon of the button" + }, + "color": { + "type": "string", + "description": "Color of the button" + } + }, + "required": [ + "type", + "prompt", + "integration_id" + ] + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "formula": "#/components/schemas/0", + "webhook": "#/components/schemas/1", + "ai": "#/components/schemas/2" + } + }, + "additionalProperties": false + }, + "FieldOptions_Links": { + "type": "object", + "title": "Links", + "properties": { + "relation_type": { + "type": "string", + "description": "Type of relationship.\n\nSupported options are listed below\n- `mm` many-to-many\n- `hm` has-many\n- `oo` one-to-one" + }, + "related_table_id": { + "type": "string", + "description": "Identifier of the linked table." + } + }, + "required": [ + "relation_type", + "related_table_id" + ], + "additionalProperties": false + }, + "FieldOptions_LinkToAnotherRecord": { + "type": "object", + "title": "LinkToAnotherRecord", + "properties": { + "relation_type": { + "type": "string", + "description": "Type of relationship.\n\nSupported options are listed below\n- `mm` many-to-many\n- `hm` has-many\n- `oo` one-to-one" + }, + "related_table_id": { + "type": "string", + "description": "Identifier of the linked table." + } + }, + "required": [ + "relation_type", + "related_table_id" + ], + "additionalProperties": false + }, + "Field": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldBase" + }, + { + "oneOf": [ + { + "properties": { + "type": { + "enum": [ + "SingleLineText" + ] + } + } + }, + { + "properties": { + "type": { + "enum": [ + "LongText" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_LongText" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "PhoneNumber", + "URL", + "Email" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_PhoneNumber" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "Number", + "Decimal" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Number" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "JSON" + ] + } + } + }, + { + "properties": { + "type": { + "enum": [ + "Currency" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Currency" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "Percent" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Percent" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "Duration" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Duration" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "Date", + "DateTime", + "Time" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_DateTime" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "SingleSelect", + "MultiSelect" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Select" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "Rating", + "Checkbox" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Rating" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "Barcode" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Barcode" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "Formula" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Formula" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "User" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_User" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "Lookup" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Lookup" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "Links" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Links" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "LinkToAnotherRecord" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_LinkToAnotherRecord" + } + } + } + ] + } + ] + }, + "FilterCreateUpdate": { + "oneOf": [ + { + "$ref": "#/components/schemas/Filter" + }, + { + "$ref": "#/components/schemas/FilterGroup" + } + ] + }, + "FieldUpdate": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldBase" + }, + { + "oneOf": [ + { + "$ref": "#/components/schemas/FieldOptions/SingleLineText" + }, + { + "title": "LongText", + "properties": { + "type": { + "enum": [ + "LongText" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_LongText" + } + } + }, + { + "title": "PhoneNumber", + "properties": { + "type": { + "enum": [ + "PhoneNumber", + "URL", + "Email" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_PhoneNumber" + } + } + }, + { + "title": "Number / Decimal", + "properties": { + "type": { + "enum": [ + "Number", + "Decimal" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Number" + } + } + }, + { + "title": "JSON", + "properties": { + "type": { + "enum": [ + "JSON" + ] + } + } + }, + { + "title": "Currency", + "properties": { + "type": { + "enum": [ + "Currency" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Currency" + } + } + }, + { + "title": "Percent", + "properties": { + "type": { + "enum": [ + "Percent" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Percent" + } + } + }, + { + "title": "Duration", + "properties": { + "type": { + "enum": [ + "Duration" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Duration" + } + } + }, + { + "title": "Date / DateTime", + "properties": { + "type": { + "enum": [ + "Date", + "DateTime", + "Time" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_DateTime" + } + } + }, + { + "title": "Single / MultiSelect", + "properties": { + "type": { + "enum": [ + "SingleSelect", + "MultiSelect" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Select" + } + } + }, + { + "title": "Checkbox", + "properties": { + "type": { + "enum": [ + "Checkbox" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Checkbox" + } + } + }, + { + "title": "Rating", + "properties": { + "type": { + "enum": [ + "Rating" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Rating" + } + } + }, + { + "title": "Barcode", + "properties": { + "type": { + "enum": [ + "Barcode" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Barcode" + } + } + }, + { + "title": "Formula", + "properties": { + "type": { + "enum": [ + "Formula" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Formula" + } + } + }, + { + "title": "User", + "properties": { + "type": { + "enum": [ + "User" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_User" + } + } + }, + { + "title": "Lookup", + "properties": { + "type": { + "enum": [ + "Lookup" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Lookup" + } + } + }, + { + "title": "Links", + "properties": { + "type": { + "enum": [ + "Links" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Links" + } + } + }, + { + "title": "LinkToAnotherRecord", + "properties": { + "type": { + "enum": [ + "LinkToAnotherRecord" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_LinkToAnotherRecord" + } + } + } + ] + } + ] + }, + "Filter": { + "type": "object", + "title": "Filter", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the filter.", + "readOnly": true + }, + "parent_id": { + "type": "string", + "description": "Parent ID of the filter, specifying this filters group association. Defaults to **root**." + }, + "field_id": { + "type": "string", + "description": "Field ID to which this filter applies." + }, + "operator": { + "type": "string", + "description": "Primary comparison operator (e.g., eq, gt, lt)." + }, + "sub_operator": { + "type": [ + "string", + "null" + ], + "description": "Secondary comparison operator (if applicable)." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value for comparison." + } + }, + "required": [ + "field_id", + "operator", + "value" + ] + }, + "FilterListResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FilterGroup" + }, + "description": "List of filter groups. Initial set of filters are mapped to a default group with group-id set to **root**." + } + }, + "required": [ + "list" + ] + }, + "FilterGroupLevel3": { + "type": "object", + "properties": { + "group_operator": { + "type": "string", + "enum": [ + "AND", + "OR" + ], + "description": "Logical operator for the group." + }, + "filters": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Filter" + }, + "description": "List of filters in this group." + } + }, + "required": [ + "group_operator", + "filters" + ] + }, + "FilterGroupLevel2": { + "type": "object", + "properties": { + "group_operator": { + "type": "string", + "enum": [ + "AND", + "OR" + ], + "description": "Logical operator for the group." + }, + "filters": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/Filter" + }, + { + "$ref": "#/components/schemas/FilterGroupLevel3" + } + ] + }, + "description": "List of filters or nested filter groups at level 3." + } + }, + "required": [ + "group_operator", + "filters" + ] + }, + "FilterGroupLevel1": { + "type": "object", + "properties": { + "group_operator": { + "type": "string", + "enum": [ + "AND", + "OR" + ], + "description": "Logical operator for the group." + }, + "filters": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/Filter" + }, + { + "$ref": "#/components/schemas/FilterGroupLevel2" + } + ] + }, + "description": "List of filters or nested filter groups at level 2." + } + }, + "required": [ + "group_operator", + "filters" + ] + }, + "FilterGroup": { + "type": "object", + "title": "FilterGroup", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the group.", + "readOnly": true + }, + "parent_id": { + "type": "string", + "description": "Parent ID of this filter-group." + }, + "group_operator": { + "type": "string", + "enum": [ + "AND", + "OR" + ], + "description": "Logical operator for combining filters in the group." + }, + "filters": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/Filter" + }, + { + "$ref": "#/components/schemas/FilterGroup" + } + ] + }, + "description": "Nested filters or filter groups." + } + }, + "required": [ + "id", + "group_operator", + "filters" + ] + }, + "FilterCreate": { + "oneOf": [ + { + "$ref": "#/components/schemas/Filter" + }, + { + "$ref": "#/components/schemas/FilterGroupLevel1" + } + ] + }, + "FilterUpdate": { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the filter." + } + }, + "required": [ + "id" + ] + }, + { + "oneOf": [ + { + "$ref": "#/components/schemas/Filter" + }, + { + "$ref": "#/components/schemas/FilterGroup" + } + ] + } + ] + }, + "SortListResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Sort" + } + } + }, + "required": [ + "list" + ] + }, + "DataRecordV3": { + "type": "object", + "description": "V3 Data Record format with id and fields separation", + "properties": { + "id": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "description": "Record identifier (primary key value)" + }, + "fields": { + "type": "object", + "description": "Record fields data (excluding primary key). Undefined when empty.", + "additionalProperties": true + } + }, + "required": [ + "id" + ] + }, + "DataRecordWithDeletedV3": { + "allOf": [ + { + "$ref": "#/components/schemas/DataRecordV3" + }, + { + "type": "object", + "properties": { + "deleted": { + "type": "boolean", + "description": "Indicates if the record was deleted" + } + }, + "required": [ + "deleted" + ] + } + ] + }, + "DataListResponseV3": { + "type": "object", + "description": "V3 Data List Response format", + "properties": { + "records": { + "type": "array", + "description": "Array of records for has-many and many-to-many relationships", + "items": { + "$ref": "#/components/schemas/DataRecordV3" + } + }, + "next": { + "type": [ + "string", + "null" + ], + "description": "Pagination token for next page" + }, + "prev": { + "type": [ + "string", + "null" + ], + "description": "Pagination token for previous page" + }, + "nestedNext": { + "type": [ + "string", + "null" + ], + "description": "Nested pagination token for next page" + }, + "nestedPrev": { + "type": [ + "string", + "null" + ], + "description": "Nested pagination token for previous page" + } + } + }, + "DataInsertRequestV3": { + "type": "object", + "description": "V3 Data Insert Request format", + "properties": { + "fields": { + "type": "object", + "description": "Record fields data", + "additionalProperties": true + } + }, + "required": [ + "fields" + ] + }, + "DataUpdateRequestV3": { + "type": "object", + "description": "V3 Data Update Request format", + "properties": { + "id": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "description": "Record identifier" + }, + "fields": { + "type": "object", + "description": "Record fields data to update", + "additionalProperties": true + } + }, + "required": [ + "id", + "fields" + ] + }, + "DataDeleteRequestV3": { + "type": "object", + "description": "Single record delete request", + "properties": { + "id": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "description": "Record identifier" + } + }, + "required": [ + "id" + ] + }, + "DataInsertResponseV3": { + "type": "object", + "description": "V3 Data Insert Response format", + "properties": { + "records": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DataRecordV3" + }, + "description": "Array of created records" + } + }, + "required": [ + "records" + ] + }, + "DataUpdateResponseV3": { + "type": "object", + "description": "V3 Data Update Response format", + "properties": { + "records": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "description": "Updated record identifier" + }, + "fields": { + "type": "object", + "description": "Record fields data (excluding primary key). Undefined when empty.", + "additionalProperties": true + } + }, + "required": [ + "id" + ] + }, + "description": "Array of updated record identifiers" + } + }, + "required": [ + "records" + ] + }, + "DataDeleteResponseV3": { + "type": "object", + "description": "V3 Data Delete Response format", + "properties": { + "records": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DataRecordWithDeletedV3" + }, + "description": "Array of deleted records" + } + }, + "required": [ + "records" + ] + }, + "DataReadResponseV3": { + "$ref": "#/components/schemas/DataRecordV3", + "description": "V3 Data Read Response format" + }, + "DataNestedListResponseV3": { + "type": "object", + "description": "V3 Nested Data List Response format - supports both single record and array responses", + "properties": { + "records": { + "type": "array", + "description": "Array of records for has-many and many-to-many relationships", + "items": { + "$ref": "#/components/schemas/DataRecordV3" + } + }, + "record": { + "description": "Single record for belongs-to and one-to-one relationships", + "oneOf": [ + { + "$ref": "#/components/schemas/DataRecordV3" + }, + { + "type": "null" + } + ] + }, + "next": { + "type": [ + "string", + "null" + ], + "description": "Pagination token for next page" + }, + "prev": { + "type": [ + "string", + "null" + ], + "description": "Pagination token for previous page" + } + } + }, + "Paginated": { + "description": "Model for Paginated", + "examples": [ + { + "next": "http://api.nocodd.com/api/v3/tables/id?page=3", + "prev": "http://api.nocodd.com/api/v3/tables/id?page=1" + } + ], + "properties": { + "next": { + "description": "URL to access next page", + "type": "string" + }, + "prev": { + "description": "URL to access previous page", + "type": "string" + }, + "nestedNext": { + "description": "URL to access current page data with next set of nested fields data", + "type": "string" + }, + "nestedPrev": { + "description": "URL to access current page data with previous set of nested fields data", + "type": "string" + } + }, + "title": "Paginated Model", + "type": "object" + }, + "BaseRoles": { + "type": "string", + "description": "Base roles for the user.", + "enum": [ + "owner", + "creator", + "editor", + "viewer", + "commenter", + "no-access" + ] + }, + "Workspace": { + "type": "object", + "description": "Basic workspace information", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the workspace" + }, + "title": { + "type": "string", + "description": "Title of the workspace" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the workspace was created" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the workspace was last updated" + } + }, + "required": [ + "id", + "title", + "created_at", + "updated_at" + ] + }, + "WorkspaceWithMembers": { + "type": "object", + "description": "Workspace information including member details", + "allOf": [ + { + "$ref": "#/components/schemas/Workspace" + }, + { + "type": "object", + "properties": { + "individual_members": { + "type": "object", + "properties": { + "workspace_members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkspaceMember" + }, + "description": "List of workspace members" + } + }, + "required": [ + "workspace_members" + ] + } + }, + "required": [ + "individual_members" + ] + } + ] + }, + "WorkspaceMember": { + "type": "object", + "description": "Individual workspace member information", + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "Email address of the member" + }, + "user_id": { + "type": "string", + "description": "Unique identifier for the user" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the user was added to the workspace" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the user was last updated in the workspace" + }, + "workspace_role": { + "$ref": "#/components/schemas/WorkspaceRoles", + "description": "Role assigned to the user in the workspace" + } + }, + "required": [ + "email", + "user_id", + "created_at", + "updated_at", + "workspace_role" + ] + }, + "WorkspaceUser": { + "type": "object", + "description": "Workspace user information", + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "Email address of the user" + }, + "user_id": { + "type": "string", + "description": "Unique identifier for the user" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the user was added to the workspace" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the user was last updated in the workspace" + }, + "workspace_role": { + "$ref": "#/components/schemas/WorkspaceRoles", + "description": "Role assigned to the user in the workspace" + } + }, + "required": [ + "email", + "user_id", + "created_at", + "updated_at", + "workspace_role" + ] + }, + "WorkspaceUserCreate": { + "type": "array", + "items": { + "type": "object", + "oneOf": [ + { + "title": "Invite User with ID", + "required": [ + "user_id", + "workspace_role" + ], + "properties": { + "user_id": { + "type": "string", + "description": "Unique identifier for the user (skip if email is provided)" + }, + "workspace_role": { + "$ref": "#/components/schemas/WorkspaceRoles", + "description": "Workspace role to assign to the user" + } + } + }, + { + "title": "Invite User with Email", + "required": [ + "email", + "workspace_role" + ], + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "Email address of the user (skip if user_id is provided)" + }, + "workspace_role": { + "$ref": "#/components/schemas/WorkspaceRoles", + "description": "Workspace role to assign to the user" + } + } + } + ], + "description": "An object representing a new workspace user to be created." + }, + "description": "Array of workspace users to be created." + }, + "WorkspaceUserUpdate": { + "type": "array", + "items": { + "type": "object", + "properties": { + "user_id": { + "type": "string", + "description": "Unique identifier for the user" + }, + "workspace_role": { + "$ref": "#/components/schemas/WorkspaceRoles", + "description": "New workspace role to assign to the user" + } + }, + "required": [ + "user_id", + "workspace_role" + ], + "description": "An object representing updates for an existing workspace user." + }, + "description": "Array of workspace user updates." + }, + "WorkspaceUserDelete": { + "type": "array", + "items": { + "type": "object", + "properties": { + "user_id": { + "type": "string", + "description": "Unique identifier for the user" + } + }, + "required": [ + "user_id" + ], + "description": "An object representing a workspace user to be deleted." + }, + "description": "Array of workspace users to be deleted." + }, + "WorkspaceRoles": { + "type": "string", + "description": "Workspace roles for the user.", + "enum": [ + "workspace-level-owner", + "workspace-level-creator", + "workspace-level-editor", + "workspace-level-viewer", + "workspace-level-commenter", + "workspace-level-no-access" + ] + } + }, + "responses": { + "BadRequest": { + "description": "BadRequest", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "msg": { + "type": "string", + "x-stoplight": { + "id": "p9mk4oi0hbihm" + }, + "example": "BadRequest [Error]: " + } + }, + "required": [ + "msg" + ] + }, + "examples": { + "Example 1": { + "value": { + "msg": "BadRequest [Error]: " + } + } + } + } + }, + "headers": {} + } + }, + "securitySchemes": { + "xc-token": { + "name": "Auth Token ", + "type": "apiKey", + "in": "header", + "description": "Auth Token is a JWT Token generated based on the logged-in user. By default, the token is only valid for 10 hours. However, you can change the value by defining it using environment variable `NC_JWT_EXPIRES_IN`." + }, + "bearerAuth": { + "name": "Authorization", + "type": "http", + "scheme": "bearer", + "description": "Bearer token authentication. Use 'Authorization: Bearer ' header format. This is an alternative to the xc-token header." + }, + "xc-shared-base-id": { + "name": "Shared Base ID", + "type": "apiKey", + "in": "header", + "description": "Shared base uuid" + }, + "xc-shared-erd-id": { + "name": "Shared ERD ID", + "type": "apiKey", + "in": "header", + "description": "Shared ERD uuid" + } + }, + "parameters": { + "xc-token": { + "name": "xc-token", + "in": "header", + "required": true, + "schema": { + "type": "string" + }, + "description": "API Token. Refer [here](https://docs.nocodb.com/account-settings/api-tokens/) to know more" + } + } + } +} diff --git a/docs/nocodb-openapi-meta-v3.json b/docs/nocodb-openapi-meta-v3.json new file mode 100644 index 0000000..81ac109 --- /dev/null +++ b/docs/nocodb-openapi-meta-v3.json @@ -0,0 +1,5567 @@ +{ + "openapi": "3.1.0", + "x-stoplight": { + "id": "qiz1rcfqd2jy6" + }, + "info": { + "title": "NocoDB", + "version": null, + "description": "NocoDB API Documentation" + }, + "servers": [ + { + "url": "https://app.nocodb.com" + } + ], + "paths": { + "/api/v3/data/{baseId}/{tableId}/records": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "baseId", + "in": "path", + "required": true, + "description": "**Base Identifier**." + }, + { + "schema": { + "type": "string" + }, + "name": "tableId", + "in": "path", + "required": true, + "description": "**Table Identifier**." + } + ], + "get": { + "summary": "List Table Records", + "operationId": "db-data-table-row-list", + "description": "This API endpoint allows you to retrieve records from a specified table. You can customize the response by applying various query parameters for filtering, sorting, and formatting.\n\n**Pagination**: The response is paginated by default, with the first page being returned initially. The response includes the following additional information in the `pageInfo` JSON block:\n\n- **next**: Contains the URL to retrieve the next page of records. For example, `\"https://app.nocodb.com/api/v3/tables/medhonywr18cysz/records?page=2\"` points to the next page of records.\n- If there are no more records available (you are on the last page), this attribute will be _null_.\n\nThe `pageInfo` attribute is particularly valuable when working with large datasets divided into multiple pages. It provides the necessary URL to seamlessly fetch subsequent pages, enabling efficient navigation through the dataset.", + "tags": [ + "Table Records" + ], + "parameters": [ + { + "schema": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string" + } + ] + }, + "in": "query", + "name": "fields", + "description": "Allows you to specify the fields that you wish to include from the linked records in your API response. By default, only Primary Key and associated display value field is included.\n\nExample: `fields=[\"field1\",\"field2\"]` or `fields=field1,field2` will include only 'field1' and 'field2' in the API response." + }, + { + "schema": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "direction": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + }, + "field": { + "type": "string" + } + }, + "required": [ + "field", + "direction" + ] + } + }, + { + "type": "object", + "properties": { + "direction": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + }, + "field": { + "type": "string" + } + }, + "required": [ + "field", + "direction" + ] + } + ] + }, + "in": "query", + "name": "sort", + "description": "Allows you to specify the fields by which you want to sort the records in your API response. Accepts either an array of sort objects or a single sort object.\n\nEach sort object must have a 'field' property specifying the field name and a 'direction' property with value 'asc' or 'desc'.\n\nExample: `sort=[{\"direction\":\"asc\",\"field\":\"field_name\"},{\"direction\":\"desc\",\"field\":\"another_field\"}]` or `sort={\"direction\":\"asc\",\"field\":\"field_name\"}`\n\nIf `viewId` query parameter is also included, the sort included here will take precedence over any sorting configuration defined in the view." + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "where", + "description": "Enables you to define specific conditions for filtering records in your API response. Multiple conditions can be combined using logical operators such as 'and' and 'or'. Each condition consists of three parts: a field name, a comparison operator, and a value.\n\nExample: `where=(field1,eq,value1)~and(field2,eq,value2)` will filter records where 'field1' is equal to 'value1' AND 'field2' is equal to 'value2'. \n\nYou can also use other comparison operators like 'neq' (not equal), 'gt' (greater than), 'lt' (less than), and more, to create complex filtering rules.\n\nIf `viewId` query parameter is also included, then the filters included here will be applied over the filtering configuration defined in the view. \n\nPlease remember to maintain the specified format, for further information on this please see [the documentation](https://nocodb.com/docs/product-docs/developer-resources/rest-apis#v3-where-query-parameter)" + }, + { + "schema": { + "type": "integer", + "minimum": 1 + }, + "in": "query", + "name": "page", + "description": "Enables you to control the pagination of your API response by specifying the page number you want to retrieve. By default, the first page is returned. If you want to retrieve the next page, you can increment the page number by one.\n\nExample: `page=2` will return the second page of records in the dataset." + }, + { + "schema": { + "type": "integer", + "minimum": 1 + }, + "in": "query", + "name": "nestedPage", + "description": "Enables you to control the pagination of your nested data (linked records) in API response by specifying the page number you want to retrieve. By default, the first page is returned. If you want to retrieve the next page, you can increment the page number by one.\n\nExample: `page=2` will return the second page of nested data records in the dataset." + }, + { + "schema": { + "type": "integer", + "minimum": 1 + }, + "in": "query", + "name": "pageSize", + "description": "Enables you to set a limit on the number of records you want to retrieve in your API response. By default, your response includes all the available records, but by using this parameter, you can control the quantity you receive.\n\nExample: `pageSize=100` will constrain your response to the first 100 records in the dataset." + }, + { + "schema": { + "type": "string" + }, + "name": "viewId", + "in": "query", + "description": "***View Identifier***. Allows you to fetch records that are currently visible within a specific view. API retrieves records in the order they are displayed if the SORT option is enabled within that view.\n\nAdditionally, if you specify a `sort` query parameter, it will take precedence over any sorting configuration defined in the view. If you specify a `where` query parameter, it will be applied over the filtering configuration defined in the view. \n\nBy default, all fields, including those that are disabled within the view, are included in the response. To explicitly specify which fields to include or exclude, you can use the `fields` query parameter to customize the output according to your requirements." + }, + { + "required": true, + "$ref": "#/components/parameters/xc-token" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataListResponseV3" + }, + "examples": { + "Example 1": { + "value": { + "records": [ + { + "id": 1, + "fields": { + "SingleLineText": "record #1", + "MultiLineText": "sample long text", + "Email": "user@nocodb.com", + "PhoneNumber": "1234567890", + "URL": "www.google.com", + "Number": 1234, + "Decimal": 100.88, + "Currency": 100, + "Percent": 10, + "Duration": 1010, + "Rating": 3, + "Year": 2020, + "Time": "20:20:00", + "Checkbox": true, + "Date": "2020-01-01", + "SingleSelect": "Jan", + "MultiSelect": [ + "Jan", + "Feb", + "Mar" + ], + "DateTime": "2022-02-02 00:00:00+00:00", + "LTAR": [ + { + "id": 1, + "fields": { + "SingleLineText": "record #1" + } + }, + { + "id": 2, + "fields": { + "SingleLineText": "record #2" + } + } + ] + } + } + ], + "next": "https://app.nocodb.com/api/v3/data/baseId/tableId/records?page=2" + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + } + } + }, + "post": { + "summary": "Create Table Records", + "operationId": "db-data-table-row-create", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataInsertResponseV3" + }, + "examples": { + "Example 1": { + "value": { + "records": [ + { + "id": 10, + "fields": { + "Number": 1, + "SingleLineText": "record #1" + } + }, + { + "id": 11, + "fields": { + "Number": 2, + "SingleLineText": "record #2" + } + } + ] + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + } + }, + "tags": [ + "Table Records" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/DataInsertRequestV3" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/DataInsertRequestV3" + } + } + ] + }, + "examples": { + "Example 1": { + "value": [ + { + "fields": { + "Number": 1, + "Decimal": 100.88, + "Currency": 100, + "Percent": 100, + "Duration": 1010, + "Rating": 3, + "Year": 2020, + "Time": "20:20:00", + "SingleLineText": "record #1", + "MultiLineText": "sample text", + "Email": "user@nocodb.com", + "PhoneNumber": "1234567890", + "URL": "www.google.com", + "SingleSelect": "jan", + "Checkbox": true, + "Date": "2020-01-01", + "JSON": { + "x": 1, + "y": 2 + }, + "User": [ + { + "email": "raju@nocodb.com" + } + ], + "MultiSelect": [ + "jan", + "feb", + "mar" + ], + "DateTime": "2022-02-02 05:30:00+00:00", + "LTAR": [ + { + "id": 1, + "fields": {} + }, + { + "id": 2, + "fields": {} + } + ] + } + } + ] + } + } + } + } + }, + "description": "This API endpoint allows the creation of new records within a specified table. Records to be inserted are input as an array of key-value pair objects, where each key corresponds to a field name. Ensure that all the required fields are included in the payload, with exceptions for fields designated as auto-increment or those having default values. \n\nCertain read-only field types will be disregarded if included in the request. These field types include Look Up, Roll Up, Formula, Created By, Updated By, Created At, Updated At, Button, Barcode and QR Code.\n\nFor **Attachment** field types, this API cannot be used. Instead, utilize the storage APIs for managing attachments. Support for attachment fields in the record update API will be added soon.", + "parameters": [ + { + "required": true, + "$ref": "#/components/parameters/xc-token" + } + ] + }, + "patch": { + "summary": "Update Table Records", + "operationId": "db-data-table-row-update", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataUpdateResponseV3" + }, + "examples": { + "Example 1": { + "value": { + "records": [ + { + "id": 6 + }, + { + "id": 7 + } + ] + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + } + }, + "tags": [ + "Table Records" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/DataUpdateRequestV3" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/DataUpdateRequestV3" + } + } + ] + }, + "examples": { + "Example 1": { + "value": [ + { + "id": 6, + "fields": { + "Number": 101, + "SingleLineText": "Updated record #1", + "Email": "updated@nocodb.com" + } + }, + { + "id": 7, + "fields": { + "Number": 102, + "SingleLineText": "Updated record #2" + } + } + ] + } + } + } + } + }, + "description": "This API endpoint allows you to update records within a specified table by their Record ID. The request payload should contain the Record ID and the fields that need to be updated.\n\nCertain read-only field types will be disregarded if included in the request. These field types include Look Up, Roll Up, Formula, Created By, Updated By, Created At, Updated At, Button, Barcode and QR Code.\n\nFor **Attachment** field types, this API cannot be used. Instead, utilize the storage APIs for managing attachments. Support for attachment fields in the record update API will be added soon.", + "parameters": [ + { + "required": true, + "$ref": "#/components/parameters/xc-token" + } + ] + }, + "delete": { + "summary": "Delete Table Records", + "operationId": "db-data-table-row-delete", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataDeleteResponseV3" + }, + "examples": { + "Example 1": { + "value": { + "records": [ + { + "id": 1, + "deleted": true + }, + { + "id": 2, + "deleted": true + } + ] + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + } + }, + "tags": [ + "Table Records" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/DataDeleteRequestV3" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/DataDeleteRequestV3" + } + } + ] + }, + "examples": { + "Example 1": { + "value": [ + { + "id": 1 + }, + { + "id": 2 + } + ] + } + } + } + } + }, + "description": "This API endpoint allows the deletion of records within a specified table by Record ID. The request should include the Record ID of the record(s) to be deleted.", + "parameters": [ + { + "required": true, + "$ref": "#/components/parameters/xc-token" + } + ] + } + }, + "/api/v3/data/{baseId}/{tableId}/records/{recordId}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "baseId", + "in": "path", + "required": true, + "description": "**Base Identifier**." + }, + { + "schema": { + "type": "string" + }, + "name": "tableId", + "in": "path", + "required": true, + "description": "**Table Identifier**" + }, + { + "schema": { + "type": "string" + }, + "name": "recordId", + "in": "path", + "required": true, + "description": "Record ID" + } + ], + "get": { + "summary": "Read Table Record", + "operationId": "db-data-table-row-read", + "description": "This API endpoint allows you to retrieve a single record identified by Record-ID, serving as unique identifier for the record from a specified table.", + "tags": [ + "Table Records" + ], + "parameters": [ + { + "schema": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string" + } + ] + }, + "in": "query", + "name": "fields", + "description": "Allows you to specify the fields that you wish to include from the linked records in your API response. By default, only Primary Key and associated display value field is included.\n\nExample: `fields=[\"field1\",\"field2\"]` or `fields=field1,field2` will include only 'field1' and 'field2' in the API response." + }, + { + "required": true, + "$ref": "#/components/parameters/xc-token" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataReadResponseV3" + }, + "examples": { + "Example 1": { + "value": { + "id": 1, + "fields": { + "SingleLineText": "record #1", + "MultiLineText": "sample long text", + "Email": "user@nocodb.com", + "PhoneNumber": "1234567890", + "URL": "www.google.com", + "Number": 1234, + "Decimal": 100.88, + "Currency": 100, + "Percent": 10, + "Duration": 1010, + "Rating": 3, + "Year": 2020, + "Time": "20:20:00", + "Checkbox": true, + "Date": "2020-01-01", + "SingleSelect": "Jan", + "MultiSelect": [ + "Jan", + "Feb", + "Mar" + ], + "DateTime": "2022-02-02 00:00:00+00:00", + "LTAR": [ + { + "id": 1, + "fields": { + "SingleLineText": "record #1" + } + }, + { + "id": 2, + "fields": { + "SingleLineText": "record #2" + } + } + ] + } + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + } + } + } + }, + "/api/v3/data/{baseId}/{modelId}/records/{recordId}/fields/{fieldId}/upload": { + "post": { + "summary": "Upload Attachment to Cell", + "operationId": "db-data-table-row-attachment-upload", + "description": "This API endpoint allows you to upload an attachment (base64 encoded) to a specific cell in a table. The attachment data includes content type, base64 encoded file, and filename.", + "tags": [ + "Table Records" + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "baseId", + "in": "path", + "required": true, + "description": "**Base Identifier**." + }, + { + "schema": { + "type": "string" + }, + "name": "modelId", + "in": "path", + "required": true, + "description": "**Model Identifier**." + }, + { + "schema": { + "type": "string" + }, + "name": "recordId", + "in": "path", + "required": true, + "description": "**Record Identifier**." + }, + { + "schema": { + "type": "string" + }, + "name": "fieldId", + "in": "path", + "required": true, + "description": "**Field Identifier**." + }, + { + "required": true, + "$ref": "#/components/parameters/xc-token" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "contentType": { + "type": "string", + "description": "Content type of the file (e.g., image/png, application/pdf)." + }, + "file": { + "type": "string", + "description": "Base64 encoded file content." + }, + "filename": { + "type": "string", + "description": "Original filename of the attachment." + } + }, + "required": [ + "contentType", + "file", + "filename" + ] + }, + "examples": { + "Example 1": { + "value": { + "contentType": "image/png", + "file": "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==", + "filename": "image.png" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataReadResponseV3" + }, + "examples": { + "Example 1": { + "value": { + "id": 1, + "fields": { + "SingleLineText": "record #1", + "MultiLineText": "sample long text", + "Email": "user@nocodb.com", + "PhoneNumber": "1234567890", + "URL": "www.google.com", + "Number": 1234, + "Decimal": 100.88, + "Currency": 100, + "Percent": 10, + "Duration": 1010, + "Rating": 3, + "Year": 2020, + "Time": "20:20:00", + "Checkbox": true, + "Date": "2020-01-01", + "SingleSelect": "Jan", + "MultiSelect": [ + "Jan", + "Feb", + "Mar" + ], + "DateTime": "2022-02-02 00:00:00+00:00", + "LTAR": [ + { + "id": 1, + "fields": { + "SingleLineText": "record #1" + } + }, + { + "id": 2, + "fields": { + "SingleLineText": "record #2" + } + } + ] + } + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + } + } + } + }, + "/api/v3/data/{baseId}/{tableId}/count": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "baseId", + "in": "path", + "required": true, + "description": "**Base Identifier**." + }, + { + "schema": { + "type": "string" + }, + "name": "tableId", + "in": "path", + "required": true, + "description": "**Table Identifier**" + }, + { + "schema": { + "type": "string" + }, + "name": "viewId", + "in": "query", + "description": "**View Identifier**. Allows you to fetch record count that are currently visible within a specific view." + } + ], + "get": { + "summary": "Count Table Records", + "operationId": "db-data-table-row-count", + "description": "This API endpoint allows you to retrieve the total number of records from a specified table or a view. You can narrow down search results by applying `where` query parameter", + "tags": [ + "Table Records" + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "where", + "description": "Enables you to define specific conditions for filtering record count in your API response. Multiple conditions can be combined using logical operators such as 'and' and 'or'. Each condition consists of three parts: a field name, a comparison operator, and a value.\n\nExample: `where=(field1,eq,value1)~and(field2,eq,value2)` will filter records where 'field1' is equal to 'value1' AND 'field2' is equal to 'value2'. \n\nYou can also use other comparison operators like 'neq' (not equal), 'gt' (greater than), 'lt' (less than), and more, to create complex filtering rules.\n\nIf `viewId` query parameter is also included, then the filters included here will be applied over the filtering configuration defined in the view. \n\nPlease remember to maintain the specified format, for further information on this please see [the documentation](https://nocodb.com/docs/product-docs/developer-resources/rest-apis#v3-where-query-parameter)" + }, + { + "$ref": "#/components/parameters/xc-token" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { + "type": "number" + } + } + }, + "examples": { + "Example 1": { + "value": { + "count": 3 + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + } + } + } + }, + "/api/v3/data/{baseId}/{tableId}/links/{linkFieldId}/{recordId}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "tableId", + "in": "path", + "required": true, + "description": "**Table Identifier**" + }, + { + "schema": { + "type": "string" + }, + "name": "linkFieldId", + "in": "path", + "required": true, + "description": "**Links Field Identifier** corresponding to the relation field `Link to another record` established between tables." + } + ], + "get": { + "summary": "List Linked Records", + "operationId": "db-data-table-row-nested-list", + "description": "This API endpoint allows you to retrieve list of linked records for a specific `Link to another record field` and `Record ID`. The response is an array of objects containing Primary Key and its corresponding display value.", + "tags": [ + "Linked Records" + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "recordId", + "in": "path", + "required": true, + "description": "**Record Identifier** corresponding to the record in this table for which linked records are being fetched." + }, + { + "schema": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string" + } + ] + }, + "in": "query", + "name": "fields", + "description": "Allows you to specify the fields that you wish to include from the linked records in your API response. By default, only Primary Key and associated display value field is included.\n\nExample: `fields=[\"field1\",\"field2\"]` or `fields=field1,field2` will include only 'field1' and 'field2' in the API response." + }, + { + "schema": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "direction": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + }, + "field": { + "type": "string" + } + }, + "required": [ + "field", + "direction" + ] + } + }, + { + "type": "object", + "properties": { + "direction": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + }, + "field": { + "type": "string" + } + }, + "required": [ + "field", + "direction" + ] + } + ] + }, + "in": "query", + "name": "sort", + "description": "Allows you to specify the fields by which you want to sort the records in your API response. Accepts either an array of sort objects or a single sort object.\n\nEach sort object must have a 'field' property specifying the field name and a 'direction' property with value 'asc' or 'desc'.\n\nExample: `sort=[{\"direction\":\"asc\",\"field\":\"field_name\"},{\"direction\":\"desc\",\"field\":\"another_field\"}]` or `sort={\"direction\":\"asc\",\"field\":\"field_name\"}`\n\nIf `viewId` query parameter is also included, the sort included here will take precedence over any sorting configuration defined in the view." + }, + { + "schema": { + "type": "string" + }, + "in": "query", + "name": "where", + "description": "Enables you to define specific conditions for filtering linked records in your API response. Multiple conditions can be combined using logical operators such as 'and' and 'or'. Each condition consists of three parts: a field name, a comparison operator, and a value.\n\nExample: `where=(field1,eq,value1)~and(field2,eq,value2)` will filter linked records where 'field1' is equal to 'value1' AND 'field2' is equal to 'value2'. \n\nYou can also use other comparison operators like 'neq' (not equal), 'gt' (greater than), 'lt' (less than), and more, to create complex filtering rules.\n\nPlease remember to maintain the specified format, for further information on this please see [the documentation](https://nocodb.com/docs/product-docs/developer-resources/rest-apis#v3-where-query-parameter)" + }, + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "in": "query", + "name": "page", + "description": "Enables you to control the pagination of your API response by specifying the page number you want to retrieve. By default, the first page is returned. If you want to retrieve the next page, you can increment the page number by one.\n\nExample: `page=2` will return the second page of linked records in the dataset." + }, + { + "schema": { + "type": "integer", + "minimum": 1 + }, + "in": "query", + "name": "pageSize", + "description": "Enables you to set a limit on the number of linked records you want to retrieve in your API response. By default, your response includes all the available linked records, but by using this parameter, you can control the quantity you receive.\n\nExample: `pageSize=100` will constrain your response to the first 100 linked records in the dataset." + }, + { + "required": true, + "$ref": "#/components/parameters/xc-token" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataListResponseV3" + }, + "examples": { + "Example 1": { + "value": { + "records": [ + { + "id": 1, + "fields": { + "SingleLineText": "record #1", + "MultiLineText": "sample long text", + "Email": "user@nocodb.com", + "PhoneNumber": "1234567890", + "URL": "www.google.com", + "Number": 1234, + "Decimal": 100.88, + "Currency": 100, + "Percent": 10, + "Duration": 1010, + "Rating": 3, + "Year": 2020, + "Time": "20:20:00", + "Checkbox": true, + "Date": "2020-01-01", + "SingleSelect": "Jan", + "MultiSelect": [ + "Jan", + "Feb", + "Mar" + ], + "DateTime": "2022-02-02 00:00:00+00:00", + "LTAR": [ + { + "id": 1, + "fields": { + "SingleLineText": "record #1" + } + }, + { + "id": 2, + "fields": { + "SingleLineText": "record #2" + } + } + ] + } + } + ], + "next": "https://app.nocodb.com/api/v3/data/baseId/tableId/records?page=2" + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + } + } + }, + "post": { + "summary": "Link Records", + "operationId": "db-data-table-row-nested-link", + "responses": { + "200": { + "description": "Records successfully linked", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "description": "Indicates whether the linking operation was successful", + "example": true + } + }, + "required": [ + "success" + ], + "additionalProperties": false + }, + "examples": { + "Success Response": { + "summary": "Successful linking operation", + "value": { + "success": true + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + } + }, + "tags": [ + "Linked Records" + ], + "requestBody": { + "required": true, + "description": "Array of record objects to be linked, each containing an id field", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the record", + "example": "33" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the record", + "example": "22" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + "minItems": 1, + "maxItems": 1000 + } + ] + }, + "examples": { + "Single Record": { + "summary": "Link a single record", + "value": { + "id": "22" + } + }, + "Multiple Records": { + "summary": "Link multiple records", + "value": [ + { + "id": "43" + }, + { + "id": "01" + } + ] + } + } + } + } + }, + "description": "This API endpoint allows you to link records to a specific `Link field` and `Record ID`. The request payload is an array of record-ids from the adjacent table for linking purposes. Note that any existing links, if present, will be unaffected during this operation.", + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "recordId", + "in": "path", + "required": true, + "description": "**Record Identifier** corresponding to the record in this table for which links are being created." + }, + { + "required": true, + "$ref": "#/components/parameters/xc-token" + } + ] + }, + "delete": { + "summary": "Unlink Records", + "operationId": "db-data-table-row-nested-unlink", + "responses": { + "200": { + "description": "Records successfully unlinked", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "description": "Indicates whether the unlink operation was successful", + "example": true + } + }, + "required": [ + "success" + ], + "additionalProperties": false + }, + "examples": { + "Success Response": { + "summary": "Successful unlinking operation", + "value": { + "success": true + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + } + }, + "tags": [ + "Linked Records" + ], + "requestBody": { + "required": true, + "description": "Array of record objects to be unlinked, each containing an id field", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the record", + "example": "33" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the record", + "example": "33" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + "minItems": 1, + "maxItems": 1000 + } + ] + }, + "examples": { + "Single Record": { + "summary": "UnLink a single record", + "value": { + "id": "32" + } + }, + "Multiple Records": { + "summary": "UnLink multiple records", + "value": [ + { + "id": "1" + }, + { + "id": "22" + } + ] + } + } + } + } + }, + "description": "This API endpoint allows you to unlink records from a specific `Link field` and `Record ID`. The request payload is an array of record-ids from the adjacent table for unlinking purposes. Note that, \n- duplicated record-ids will be ignored.\n- non-existent record-ids will be ignored.", + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "recordId", + "in": "path", + "required": true, + "description": "**Record Identifier** corresponding to the record in this table for which links are being removed." + }, + { + "required": true, + "$ref": "#/components/parameters/xc-token" + } + ] + } + } + }, + "components": { + "schemas": { + "0": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "formula" + ], + "description": "Button type: formula" + }, + "formula": { + "type": "string", + "description": "Formula to execute" + } + }, + "required": [ + "type", + "formula" + ] + }, + "1": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "webhook" + ], + "description": "Button type: webhook" + }, + "button_hook_id": { + "type": "string", + "description": "ID of the webhook to trigger" + } + }, + "required": [ + "type", + "button_hook_id" + ] + }, + "2": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ai" + ], + "description": "Button type: AI" + }, + "prompt": { + "type": "string", + "description": "AI prompt to execute" + }, + "integration_id": { + "type": "string", + "description": "Integration ID for AI service" + }, + "theme": { + "type": "string", + "description": "Theme of the button" + }, + "output_column_ids": { + "type": "string", + "description": "IDs of columns where AI output should be stored" + }, + "label": { + "type": "string", + "description": "Label of the button" + }, + "icon": { + "type": "string", + "description": "Icon of the button" + }, + "color": { + "type": "string", + "description": "Color of the button" + } + }, + "required": [ + "type", + "prompt", + "integration_id" + ] + }, + "Base": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the base." + }, + "title": { + "type": "string", + "description": "Title of the base." + }, + "meta": { + "$ref": "#/components/schemas/BaseMetaRes" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp of when the base was created." + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp of when the base was last updated." + }, + "workspace_id": { + "type": "string", + "description": "Unique identifier for the workspace to which this base belongs to." + }, + "sources": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the data source." + }, + "title": { + "type": "string", + "description": "Title of the data source." + }, + "type": { + "type": "string", + "description": "Type of the data source (e.g., pg, mysql)." + }, + "is_schema_readonly": { + "type": "boolean", + "description": "Indicates if the schema in this data source is read-only." + }, + "is_data_readonly": { + "type": "boolean", + "description": "Indicates if the data (records) in this data source is read-only." + }, + "integration_id": { + "type": "string", + "description": "Integration ID for the data source." + } + }, + "required": [ + "id", + "title", + "type", + "is_schema_readonly", + "is_data_readonly", + "integration_id" + ] + }, + "description": "List of data sources associated with this base. This information will be included only if one or more external data sources are associated with the base." + } + }, + "required": [ + "id", + "title", + "meta", + "created_at", + "updated_at", + "workspace_id" + ] + }, + "BaseWithMembers": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the base." + }, + "title": { + "type": "string", + "description": "Title of the base." + }, + "meta": { + "$ref": "#/components/schemas/BaseMetaRes" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp of when the base was created." + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp of when the base was last updated." + }, + "workspace_id": { + "type": "string", + "description": "Unique identifier for the workspace to which this base belongs to." + }, + "sources": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the data source." + }, + "title": { + "type": "string", + "description": "Title of the data source." + }, + "type": { + "type": "string", + "description": "Type of the data source (e.g., pg, mysql)." + }, + "is_schema_readonly": { + "type": "boolean", + "description": "Indicates if the schema in this data source is read-only." + }, + "is_data_readonly": { + "type": "boolean", + "description": "Indicates if the data (records) in this data source is read-only." + }, + "integration_id": { + "type": "string", + "description": "Integration ID for the data source." + } + }, + "required": [ + "id", + "title", + "type", + "is_schema_readonly", + "is_data_readonly", + "integration_id" + ] + }, + "description": "List of data sources associated with this base. This information will be included only if one or more external data sources are associated with the base." + }, + "individual_members": { + "type": "object", + "properties": { + "base_members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseMemberWithWorkspaceRole" + } + }, + "workspace_members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkspaceMember" + } + } + } + } + }, + "required": [ + "id", + "title", + "meta", + "created_at", + "updated_at", + "workspace_id" + ] + }, + "BaseMetaRes": { + "type": "object", + "properties": { + "icon_color": { + "type": "string", + "description": "Specifies the color of the base icon using a hexadecimal color code (e.g., `#36BFFF`)", + "pattern": "^#[0-9A-Fa-f]{6}$" + } + } + }, + "BaseMetaReq": { + "type": "object", + "properties": { + "icon_color": { + "type": "string", + "description": "Specifies the color of the base icon using a hexadecimal color code (e.g., `#36BFFF`).\n\n**Constraints**:\n- Must be a valid 6-character hexadecimal color code preceded by a `#`.\n- Optional field; defaults to a standard color if not provided.", + "pattern": "^#[0-9A-Fa-f]{6}$" + } + } + }, + "BaseCreate": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Title of the base." + }, + "meta": { + "$ref": "#/components/schemas/BaseMetaReq" + } + }, + "required": [ + "title" + ] + }, + "BaseUpdate": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Title of the base." + }, + "meta": { + "$ref": "#/components/schemas/BaseMetaReq" + } + } + }, + "TableList": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the table." + }, + "title": { + "type": "string", + "description": "Title of the table." + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "Description of the table." + }, + "meta": { + "$ref": "#/components/schemas/TableMeta" + }, + "base_id": { + "type": "string", + "description": "Unique identifier for the base to which this table belongs to." + }, + "source_id": { + "type": "string", + "description": "Unique identifier for the data source. This information will be included only if the table is associated with an external data source." + }, + "workspace_id": { + "type": "string", + "description": "Unique identifier for the workspace to which this base belongs to." + } + }, + "required": [ + "id", + "title", + "base_id", + "workspace_id" + ] + } + } + }, + "required": [ + "list" + ] + }, + "TableMeta": { + "type": "object", + "properties": { + "icon": { + "type": "string", + "description": "Icon prefix to the table name that needs to be displayed in-lieu of the default table icon." + } + } + }, + "TableCreate": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Title of the table." + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "Description of the table." + }, + "meta": { + "$ref": "#/components/schemas/TableMeta" + }, + "source_id": { + "type": "string", + "description": "Unique identifier for the data source. Include this information only if the table being created is part of a data source." + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CreateField" + } + } + }, + "required": [ + "title" + ] + }, + "FieldOptions": { + "SingleLineText": { + "title": "SingleLineText", + "properties": { + "type": { + "enum": [ + "SingleLineText" + ] + } + } + }, + "LongText": { + "title": "LongText", + "properties": { + "type": { + "enum": [ + "LongText" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_LongText" + } + } + }, + "PhoneNumber": { + "title": "PhoneNumber", + "properties": { + "type": { + "enum": [ + "PhoneNumber" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_PhoneNumber" + } + } + }, + "URL": { + "title": "URL", + "properties": { + "type": { + "enum": [ + "URL" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_URL" + } + } + }, + "Email": { + "title": "Email", + "properties": { + "type": { + "enum": [ + "Email" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Email" + } + } + }, + "Number": { + "title": "Number", + "properties": { + "type": { + "enum": [ + "Number" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Number" + } + } + }, + "Decimal": { + "title": "Decimal", + "properties": { + "type": { + "enum": [ + "Decimal" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Decimal" + } + } + }, + "Currency": { + "title": "Currency", + "properties": { + "type": { + "enum": [ + "Currency" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Currency" + } + } + }, + "Percent": { + "title": "Percent", + "properties": { + "type": { + "enum": [ + "Percent" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Percent" + } + } + }, + "Duration": { + "title": "Duration", + "properties": { + "type": { + "enum": [ + "Duration" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Duration" + } + } + }, + "Date": { + "title": "Date", + "properties": { + "type": { + "enum": [ + "Date" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Date" + } + } + }, + "DateTime": { + "title": "DateTime", + "properties": { + "type": { + "enum": [ + "DateTime" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_DateTime" + } + } + }, + "Time": { + "title": "Time", + "properties": { + "type": { + "enum": [ + "Time" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Time" + } + } + }, + "SingleSelect": { + "title": "SingleSelect", + "properties": { + "type": { + "enum": [ + "SingleSelect" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Select" + } + } + }, + "MultiSelect": { + "title": "MultiSelect", + "properties": { + "type": { + "enum": [ + "MultiSelect" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Select" + } + } + }, + "Rating": { + "title": "Rating", + "properties": { + "type": { + "enum": [ + "Rating" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Rating" + } + } + }, + "Checkbox": { + "title": "Checkbox", + "properties": { + "type": { + "enum": [ + "Checkbox" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Checkbox" + } + } + }, + "Attachment": { + "title": "Attachment", + "properties": { + "type": { + "enum": [ + "Attachment" + ] + } + } + }, + "Geometry": { + "title": "Geometry", + "properties": { + "type": { + "enum": [ + "Geometry" + ] + } + } + }, + "Links": { + "title": "Links", + "properties": { + "type": { + "enum": [ + "Links" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Links" + } + } + }, + "Lookup": { + "title": "Lookup", + "properties": { + "type": { + "enum": [ + "Lookup" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Lookup" + } + } + }, + "Rollup": { + "title": "Rollup", + "properties": { + "type": { + "enum": [ + "Rollup" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Rollup" + } + } + }, + "Button": { + "title": "Button", + "properties": { + "type": { + "enum": [ + "Button" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Button" + } + } + }, + "Formula": { + "title": "Formula", + "properties": { + "type": { + "enum": [ + "Formula" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Formula" + } + } + }, + "Barcode": { + "title": "Barcode", + "properties": { + "type": { + "enum": [ + "Barcode" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Barcode" + } + } + }, + "Year": { + "title": "Year", + "properties": { + "type": { + "enum": [ + "Year" + ] + } + } + }, + "QrCode": { + "title": "QrCode", + "properties": { + "type": { + "enum": [ + "QrCode" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_QrCode" + } + } + }, + "CreatedTime": { + "title": "CreatedTime", + "properties": { + "type": { + "enum": [ + "CreatedTime" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_DateTime" + } + } + }, + "LastModifiedTime": { + "title": "LastModifiedTime", + "properties": { + "type": { + "enum": [ + "LastModifiedTime" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_DateTime" + } + } + }, + "CreatedBy": { + "title": "CreatedBy", + "properties": { + "type": { + "enum": [ + "CreatedBy" + ] + } + } + }, + "LastModifiedBy": { + "title": "LastModifiedBy", + "properties": { + "type": { + "enum": [ + "LastModifiedBy" + ] + } + } + }, + "LinkToAnotherRecord": { + "title": "LinkToAnotherRecord", + "properties": { + "type": { + "enum": [ + "LinkToAnotherRecord" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_LinkToAnotherRecord" + } + } + }, + "User": { + "title": "User", + "properties": { + "type": { + "enum": [ + "User" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_User" + } + } + }, + "JSON": { + "title": "JSON", + "properties": { + "type": { + "enum": [ + "JSON" + ] + } + } + } + }, + "CreateField": { + "allOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/FieldOptions/SingleLineText" + }, + { + "$ref": "#/components/schemas/FieldOptions/LongText" + }, + { + "$ref": "#/components/schemas/FieldOptions/PhoneNumber" + }, + { + "$ref": "#/components/schemas/FieldOptions/URL" + }, + { + "$ref": "#/components/schemas/FieldOptions/Email" + }, + { + "$ref": "#/components/schemas/FieldOptions/Number" + }, + { + "$ref": "#/components/schemas/FieldOptions/Decimal" + }, + { + "$ref": "#/components/schemas/FieldOptions/Currency" + }, + { + "$ref": "#/components/schemas/FieldOptions/Percent" + }, + { + "$ref": "#/components/schemas/FieldOptions/Duration" + }, + { + "$ref": "#/components/schemas/FieldOptions/Date" + }, + { + "$ref": "#/components/schemas/FieldOptions/DateTime" + }, + { + "$ref": "#/components/schemas/FieldOptions/Time" + }, + { + "$ref": "#/components/schemas/FieldOptions/Year" + }, + { + "$ref": "#/components/schemas/FieldOptions/SingleSelect" + }, + { + "$ref": "#/components/schemas/FieldOptions/MultiSelect" + }, + { + "$ref": "#/components/schemas/FieldOptions/Rating" + }, + { + "$ref": "#/components/schemas/FieldOptions/Checkbox" + }, + { + "$ref": "#/components/schemas/FieldOptions/Attachment" + }, + { + "$ref": "#/components/schemas/FieldOptions/JSON" + }, + { + "$ref": "#/components/schemas/FieldOptions/Geometry" + }, + { + "$ref": "#/components/schemas/FieldOptions/Links" + }, + { + "$ref": "#/components/schemas/FieldOptions/Lookup" + }, + { + "$ref": "#/components/schemas/FieldOptions/Rollup" + }, + { + "$ref": "#/components/schemas/FieldOptions/Button" + }, + { + "$ref": "#/components/schemas/FieldOptions/Formula" + }, + { + "$ref": "#/components/schemas/FieldOptions/Barcode" + }, + { + "$ref": "#/components/schemas/FieldOptions/QrCode" + }, + { + "$ref": "#/components/schemas/FieldOptions/CreatedTime" + }, + { + "$ref": "#/components/schemas/FieldOptions/LastModifiedTime" + }, + { + "$ref": "#/components/schemas/FieldOptions/CreatedBy" + }, + { + "$ref": "#/components/schemas/FieldOptions/LastModifiedBy" + }, + { + "$ref": "#/components/schemas/FieldOptions/LinkToAnotherRecord" + }, + { + "$ref": "#/components/schemas/FieldOptions/User" + } + ] + }, + { + "$ref": "#/components/schemas/FieldBaseCreate" + } + ] + }, + "Table": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the table." + }, + "source_id": { + "type": "string", + "description": "Unique identifier for the data source. This information will be included only if the table is associated with an external data source." + }, + "base_id": { + "type": "string", + "description": "Unique identifier for the base to which this table belongs to." + }, + "title": { + "type": "string", + "description": "Title of the table." + }, + "description": { + "type": "string", + "description": "Description of the table." + }, + "display_field_id": { + "type": "string", + "description": "Unique identifier for the display field of the table. First non system field is set as display field by default." + }, + "workspace_id": { + "type": "string", + "description": "Unique identifier for the workspace to which this base belongs to." + }, + "fields": { + "type": "array", + "description": "List of fields associated with this table.", + "items": { + "$ref": "#/components/schemas/CreateField" + } + }, + "views": { + "type": "array", + "description": "List of views associated with this table.", + "items": { + "$ref": "#/components/schemas/ViewSummary" + } + } + }, + "required": [ + "id", + "title", + "base_id", + "workspace_id", + "display_field_id", + "fields", + "views" + ] + }, + "BaseMember": { + "type": "object", + "properties": { + "user_id": { + "type": "string", + "description": "Unique identifier for the user." + }, + "email": { + "type": "string", + "format": "email", + "description": "Email address of the user." + }, + "user_name": { + "type": "string", + "description": "Display name of the user." + }, + "base_role": { + "$ref": "#/components/schemas/BaseRoles" + } + }, + "required": [ + "user_id", + "email", + "created_at", + "updated_at", + "base_role" + ] + }, + "BaseMemberWithWorkspaceRole": { + "type": "object", + "properties": { + "user_id": { + "type": "string", + "description": "Unique identifier for the user." + }, + "email": { + "type": "string", + "format": "email", + "description": "Email address of the user." + }, + "user_name": { + "type": "string", + "description": "Display name of the user." + }, + "base_role": { + "$ref": "#/components/schemas/BaseRoles" + }, + "workspace_role": { + "$ref": "#/components/schemas/WorkspaceRoles", + "description": "Role assigned to the user in the workspace" + } + }, + "required": [ + "user_id", + "email", + "created_at", + "updated_at", + "base_role" + ] + }, + "BaseUserDeleteRequest": {}, + "BaseMemberList": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BaseMember" + } + } + }, + "required": [ + "users" + ] + }, + "BaseMemberCreate": { + "type": "array", + "items": { + "type": "object", + "properties": { + "base_role": { + "$ref": "#/components/schemas/BaseRoles" + } + }, + "required": [ + "base_role" + ], + "oneOf": [ + { + "title": "Invite User with ID", + "required": [ + "base_role", + "user_id" + ], + "properties": { + "user_id": { + "type": "string", + "description": "Unique identifier for the user (skip if email is provided)" + }, + "user_name": { + "type": "string", + "description": "Full name of the user." + } + } + }, + { + "title": "Invite User with Email", + "required": [ + "base_role", + "email" + ], + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "Email address of the user (skip if user_id is provided)" + }, + "user_name": { + "type": "string", + "description": "Full name of the user." + } + } + } + ], + "description": "An object representing a new member to be created." + }, + "description": "Array of members to be created." + }, + "BaseMemberUpdate": { + "type": "array", + "items": { + "type": "object", + "properties": { + "user_id": { + "type": "string", + "description": "Unique user identifier for the member." + }, + "base_role": { + "$ref": "#/components/schemas/BaseRoles" + } + }, + "required": [ + "user_id", + "base_role" + ], + "description": "An object representing updates for an existing member." + }, + "description": "Array of member updates." + }, + "BaseMemberDelete": { + "type": "array", + "items": { + "type": "object", + "properties": { + "user_id": { + "type": "string", + "description": "User unique identifier for the member." + } + }, + "required": [ + "user_id" + ] + } + }, + "TableMetaReq": { + "type": "object", + "properties": { + "icon": { + "type": "string", + "description": "Icon prefix to the table name that needs to be displayed in-lieu of the default table icon." + } + } + }, + "TableUpdate": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "New title of the table." + }, + "description": { + "type": "string", + "description": "Description of the table." + }, + "display_field_id": { + "type": "string", + "description": "Unique identifier for the display field of the table. The type of the field should be one of the allowed types for display field." + }, + "meta": { + "$ref": "#/components/schemas/TableMetaReq", + "description": "Icon prefix to the table name that needs to be displayed in-lieu of the default table icon." + } + }, + "oneOf": [ + { + "title": "Rename Table", + "required": [ + "title" + ] + }, + { + "title": "Update Table Description", + "required": [ + "description" + ] + }, + { + "title": "Update Display Field", + "required": [ + "display_field_id" + ] + }, + { + "title": "Update Table Icon", + "required": [ + "meta" + ] + } + ] + }, + "Sort": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the sort.", + "readOnly": true + }, + "field_id": { + "type": "string", + "format": "uuid", + "description": "Identifier for the field being sorted." + }, + "direction": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "description": "Sorting direction, either 'asc' (ascending) or 'desc' (descending)." + } + }, + "required": [ + "id", + "field_id", + "direction" + ] + }, + "SortCreate": { + "type": "object", + "properties": { + "field_id": { + "type": "string", + "description": "Identifier for the field being sorted." + }, + "direction": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "description": "Sorting direction, either 'asc' (ascending) or 'desc' (descending)." + } + }, + "required": [ + "field_id" + ] + }, + "SortUpdate": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the sort." + }, + "field_id": { + "type": "string", + "format": "uuid", + "description": "Identifier for the field being sorted." + }, + "direction": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "description": "Sorting direction, either 'asc' (ascending) or 'desc' (descending)." + } + }, + "required": [ + "id" + ] + }, + "ViewSummary": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "description": "Unique identifier for the view." + }, + "title": { + "type": "string", + "description": "Name of the view." + }, + "view_type": { + "type": "string", + "enum": [ + "grid", + "gallery", + "kanban", + "calendar", + "form" + ], + "description": "Type of the view." + } + } + }, + "ViewAggregationEnum": { + "type": "string", + "enum": [ + "sum", + "min", + "max", + "avg", + "median", + "std_dev", + "range", + "count", + "count_empty", + "count_filled", + "count_unique", + "percent_empty", + "percent_filled", + "percent_unique", + "none", + "attachment_size", + "checked", + "unchecked", + "percent_checked", + "percent_unchecked", + "earliest_date", + "latest_date", + "date_range", + "month_range" + ] + }, + "ViewList": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the view." + }, + "table_id": { + "type": "string", + "description": "Id of table associated with the view." + }, + "title": { + "type": "string", + "description": "Title of the view." + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "Description of the view." + }, + "type": { + "type": "string", + "enum": [ + "grid", + "gallery", + "kanban", + "calendar", + "form" + ], + "description": "Type of the view." + }, + "lock_type": { + "type": "string", + "enum": [ + "collaborative", + "locked", + "personal" + ], + "description": "View configuration edit state." + }, + "is_default": { + "type": "boolean", + "description": "Indicates if this is the default view." + }, + "created_by": { + "type": "string", + "description": "User ID of the creator." + }, + "owned_by": { + "type": "string", + "description": "User ID of the owner. Applicable only for personal views." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp of creation." + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp of last update." + } + }, + "required": [ + "id", + "title", + "type", + "lock_type", + "created_at", + "updated_at", + "created_by" + ] + } + } + }, + "required": [ + "list" + ] + }, + "ViewBase": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Title of the view." + }, + "type": { + "type": "string", + "enum": [ + "grid", + "gallery", + "kanban", + "calendar" + ], + "description": "Type of the view. \n\nNote: Form view via API is not supported currently" + }, + "lock_type": { + "type": "string", + "enum": [ + "collaborative", + "locked", + "personal" + ], + "description": "Lock type of the view.\n\n Note: Assigning view as personal using API is not supported currently", + "default": "collaborative" + }, + "description": { + "type": "string", + "description": "Description of the view." + } + }, + "required": [ + "title", + "type" + ] + }, + "ViewBaseInUpdate": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Title of the view." + }, + "lock_type": { + "type": "string", + "enum": [ + "collaborative", + "locked", + "personal" + ], + "description": "Lock type of the view.\n\n Note: Assigning view as personal using API is not supported currently", + "default": "collaborative" + }, + "description": { + "type": "string", + "description": "Description of the view." + } + } + }, + "ViewFields": { + "type": "array", + "items": { + "type": "object", + "properties": { + "field_id": { + "type": "string", + "description": "Unique identifier for the field." + }, + "show": { + "type": "boolean", + "description": "Indicates whether the field should be displayed in the view." + }, + "width": { + "type": "integer", + "description": "Width of the field in pixels.\n\n **Applicable only for grid view.**" + }, + "aggregation": { + "$ref": "#/components/schemas/ViewAggregationEnum", + "description": "Aggregation function to be applied to the field.\n\n **Applicable only for grid view.**" + } + }, + "required": [ + "field_id", + "show" + ] + }, + "description": "List of fields to be displayed in the view. \n\n- If not specified, all fields are displayed by default.\n- If an empty array is provided, only the display value field will be shown.\n- In case of partial list, fields not included in the list will be excluded from the view." + }, + "ViewRowColour": { + "type": "object", + "oneOf": [ + { + "type": "object", + "title": "conditions", + "properties": { + "mode": { + "type": "string", + "enum": [ + "filter" + ], + "description": "Mode of row coloring. In this mode, the color is selected based on conditions applied to the fields." + }, + "conditions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "apply_as_row_background": { + "type": "boolean" + }, + "color": { + "type": "string" + }, + "filters": { + "$ref": "#/components/schemas/FilterCreateUpdate" + } + } + } + } + }, + "required": [ + "mode", + "conditions" + ] + }, + { + "type": "object", + "title": "select", + "properties": { + "mode": { + "type": "string", + "enum": [ + "select" + ], + "description": "Mode of row coloring. In this mode, the color is selected based on a single select field." + }, + "field_id": { + "type": "string", + "description": "Single select field ID to be used for colouring rows in the view." + }, + "apply_as_row_background": { + "type": "boolean", + "description": "Whether to additionally apply the color as row background." + } + }, + "required": [ + "mode", + "field_id" + ] + } + ], + "discriminator": { + "propertyName": "mode" + } + }, + "ViewOptionsGrid": { + "type": "object", + "properties": { + "groups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "field_id": { + "type": "string", + "description": "Identifier for the field being sorted." + }, + "direction": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "description": "Direction of the group, either 'asc' (ascending) or 'desc' (descending).", + "default": "asc" + } + }, + "required": [ + "field_id" + ] + }, + "description": "List of groups to be applied on the grid view." + }, + "row_height": { + "type": "string", + "enum": [ + "short", + "medium", + "tall", + "extra" + ], + "description": "Height of the rows in the grid view.", + "default": "short" + } + } + }, + "ViewOptionsKanban": { + "type": "object", + "properties": { + "stack_by": { + "type": "object", + "properties": { + "field_id": { + "type": "string", + "description": "Single select field ID to be used for stacking cards in kanban view." + }, + "stack_order": { + "type": "array", + "description": "Order of the stacks in kanban view. If not provided, the order will be determined by options listed in associated field.\n\nExample: ```stack_order: ['option1', 'option2', 'option3']```", + "items": { + "type": "string" + } + } + }, + "required": [ + "field_id" + ] + }, + "cover_field_id": { + "type": "string", + "description": "Attachment field ID to be used as cover image in kanban view. If not provided, cover field configuration is skipped." + } + }, + "required": [ + "stack_by" + ] + }, + "ViewOptionsCalendar": { + "type": "object", + "properties": { + "date_ranges": { + "type": "array", + "items": { + "type": "object", + "properties": { + "start_date_field_id": { + "type": "string", + "description": "Date field ID to be used as start date in calendar view." + }, + "end_date_field_id": { + "type": "string", + "description": "Date field ID to be used as end date in calendar view." + } + }, + "required": [ + "start_date_field_id" + ] + } + } + }, + "required": [ + "date_ranges" + ] + }, + "ViewOptionsGallery": { + "type": "object", + "properties": { + "cover_field_id": { + "type": "string", + "description": "Attachment field ID to be used as cover image in gallery view. Is optional, if not provided, the first attachment field will be used." + } + } + }, + "ViewOptionsForm": { + "type": "object", + "properties": { + "form_title": { + "type": "string", + "description": "Heading for the form." + }, + "form_description": { + "type": "string", + "description": "Subheading for the form." + }, + "thank_you_message": { + "type": "string", + "description": "Success message shown after form submission." + }, + "form_redirect_after_secs": { + "type": "integer", + "description": "Seconds to wait before redirecting." + }, + "show_submit_another_button": { + "type": "boolean", + "description": "Whether to show another form after submission." + }, + "reset_form_after_submit": { + "type": "boolean", + "description": "Whether to show a blank form after submission." + }, + "form_hide_banner": { + "type": "boolean", + "description": "Whether to hide the banner on the form." + }, + "form_hide_branding": { + "type": "boolean", + "description": "Whether to hide branding on the form." + }, + "banner": { + "type": "string", + "format": "uri", + "description": "URL of the banner image for the form." + }, + "logo": { + "type": "string", + "format": "uri", + "description": "URL of the logo for the form." + }, + "form_background_color": { + "type": "string", + "description": "Background color for the form.", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "redirect_url": { + "type": "string", + "format": "uri", + "description": "URL to redirect to after form submission." + } + } + }, + "ViewCreate": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/ViewBase" + }, + { + "oneOf": [ + { + "type": "object", + "title": "grid", + "properties": { + "type": { + "type": "string", + "enum": [ + "grid" + ] + }, + "options": { + "$ref": "#/components/schemas/ViewOptionsGrid" + }, + "sorts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortCreate" + }, + "description": "List of sorts to be applied to the view." + }, + "filters": { + "$ref": "#/components/schemas/FilterCreateUpdate" + }, + "fields": { + "$ref": "#/components/schemas/ViewFields" + }, + "row_coloring": { + "$ref": "#/components/schemas/ViewRowColour", + "description": "Row colour configuration for the the view." + } + } + }, + { + "type": "object", + "title": "gallery", + "properties": { + "type": { + "type": "string", + "enum": [ + "gallery" + ] + }, + "options": { + "$ref": "#/components/schemas/ViewOptionsGallery" + }, + "sorts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortCreate" + }, + "description": "List of sorts to be applied to the view." + }, + "filters": { + "$ref": "#/components/schemas/FilterCreateUpdate" + }, + "fields": { + "$ref": "#/components/schemas/ViewFields" + }, + "row_coloring": { + "$ref": "#/components/schemas/ViewRowColour", + "description": "Row colour configuration for the the view." + } + } + }, + { + "type": "object", + "title": "kanban", + "properties": { + "type": { + "type": "string", + "enum": [ + "kanban" + ] + }, + "options": { + "$ref": "#/components/schemas/ViewOptionsKanban" + }, + "sorts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortCreate" + }, + "description": "List of sorts to be applied to the view." + }, + "filters": { + "$ref": "#/components/schemas/FilterCreateUpdate" + }, + "fields": { + "$ref": "#/components/schemas/ViewFields" + }, + "row_coloring": { + "$ref": "#/components/schemas/ViewRowColour", + "description": "Row colour configuration for the the view." + } + }, + "required": [ + "options" + ] + }, + { + "type": "object", + "title": "calendar", + "properties": { + "type": { + "type": "string", + "enum": [ + "calendar" + ] + }, + "options": { + "$ref": "#/components/schemas/ViewOptionsCalendar" + }, + "sorts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortCreate" + }, + "description": "List of sorts to be applied to the view." + }, + "filters": { + "$ref": "#/components/schemas/FilterCreateUpdate" + }, + "fields": { + "$ref": "#/components/schemas/ViewFields" + }, + "row_coloring": { + "$ref": "#/components/schemas/ViewRowColour", + "description": "Row colour configuration for the the view." + } + }, + "required": [ + "options" + ] + } + ], + "discriminator": { + "propertyName": "type" + } + } + ] + }, + "ViewUpdate": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/ViewBaseInUpdate" + }, + { + "oneOf": [ + { + "type": "object", + "title": "grid", + "properties": { + "options": { + "$ref": "#/components/schemas/ViewOptionsGrid" + }, + "sorts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortCreate" + }, + "description": "List of sorts to be applied to the view." + }, + "filters": { + "$ref": "#/components/schemas/FilterCreateUpdate" + }, + "fields": { + "$ref": "#/components/schemas/ViewFields" + }, + "row_coloring": { + "$ref": "#/components/schemas/ViewRowColour", + "description": "Row colour configuration for the the view." + } + } + }, + { + "type": "object", + "title": "gallery", + "properties": { + "options": { + "$ref": "#/components/schemas/ViewOptionsGallery" + }, + "sorts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortCreate" + }, + "description": "List of sorts to be applied to the view." + }, + "filters": { + "$ref": "#/components/schemas/FilterCreateUpdate" + }, + "fields": { + "$ref": "#/components/schemas/ViewFields" + }, + "row_coloring": { + "$ref": "#/components/schemas/ViewRowColour", + "description": "Row colour configuration for the the view." + } + } + }, + { + "type": "object", + "title": "kanban", + "properties": { + "options": { + "$ref": "#/components/schemas/ViewOptionsKanban" + }, + "sorts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortCreate" + }, + "description": "List of sorts to be applied to the view." + }, + "filters": { + "$ref": "#/components/schemas/FilterCreateUpdate" + }, + "fields": { + "$ref": "#/components/schemas/ViewFields" + }, + "row_coloring": { + "$ref": "#/components/schemas/ViewRowColour", + "description": "Row colour configuration for the the view." + } + } + }, + { + "type": "object", + "title": "calendar", + "properties": { + "options": { + "$ref": "#/components/schemas/ViewOptionsCalendar" + }, + "sorts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortCreate" + }, + "description": "List of sorts to be applied to the view." + }, + "filters": { + "$ref": "#/components/schemas/FilterCreateUpdate" + }, + "fields": { + "$ref": "#/components/schemas/ViewFields" + }, + "row_coloring": { + "$ref": "#/components/schemas/ViewRowColour", + "description": "Row colour configuration for the the view." + } + } + } + ], + "discriminator": { + "propertyName": "type" + } + } + ] + }, + "View": { + "type": "object", + "allOf": [ + { + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the view." + }, + "table_id": { + "type": "string", + "description": "Id of table associated with the view." + }, + "is_default": { + "type": "boolean", + "description": "Indicates if this is the default view. Omitted if not the default view." + } + }, + "required": [ + "id" + ] + }, + { + "$ref": "#/components/schemas/ViewBase" + }, + { + "properties": { + "created_by": { + "type": "string", + "description": "User ID of the creator." + }, + "owned_by": { + "type": "string", + "description": "User ID of the owner." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp of creation." + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp of last update." + } + } + }, + { + "oneOf": [ + { + "type": "object", + "title": "grid", + "properties": { + "type": { + "type": "string", + "enum": [ + "grid" + ] + }, + "options": { + "$ref": "#/components/schemas/ViewOptionsGrid" + }, + "sorts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortCreate" + }, + "description": "List of sorts to be applied to the view." + }, + "filters": { + "$ref": "#/components/schemas/FilterCreateUpdate" + }, + "fields": { + "$ref": "#/components/schemas/ViewFields" + }, + "row_coloring": { + "$ref": "#/components/schemas/ViewRowColour", + "description": "Row colour configuration for the the view." + } + } + }, + { + "type": "object", + "title": "gallery", + "properties": { + "type": { + "type": "string", + "enum": [ + "gallery" + ] + }, + "options": { + "$ref": "#/components/schemas/ViewOptionsGallery" + }, + "sorts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortCreate" + }, + "description": "List of sorts to be applied to the view." + }, + "filters": { + "$ref": "#/components/schemas/FilterCreateUpdate" + }, + "fields": { + "$ref": "#/components/schemas/ViewFields" + }, + "row_coloring": { + "$ref": "#/components/schemas/ViewRowColour", + "description": "Row colour configuration for the the view." + } + } + }, + { + "type": "object", + "title": "kanban", + "properties": { + "type": { + "type": "string", + "enum": [ + "kanban" + ] + }, + "options": { + "$ref": "#/components/schemas/ViewOptionsKanban" + }, + "sorts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortCreate" + }, + "description": "List of sorts to be applied to the view." + }, + "filters": { + "$ref": "#/components/schemas/FilterCreateUpdate" + }, + "fields": { + "$ref": "#/components/schemas/ViewFields" + }, + "row_coloring": { + "$ref": "#/components/schemas/ViewRowColour", + "description": "Row colour configuration for the the view." + } + }, + "required": [ + "options" + ] + }, + { + "type": "object", + "title": "calendar", + "properties": { + "type": { + "type": "string", + "enum": [ + "calendar" + ] + }, + "options": { + "$ref": "#/components/schemas/ViewOptionsCalendar" + }, + "sorts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortCreate" + }, + "description": "List of sorts to be applied to the view." + }, + "filters": { + "$ref": "#/components/schemas/FilterCreateUpdate" + }, + "fields": { + "$ref": "#/components/schemas/ViewFields" + }, + "row_coloring": { + "$ref": "#/components/schemas/ViewRowColour", + "description": "Row colour configuration for the the view." + } + }, + "required": [ + "options" + ] + } + ], + "discriminator": { + "propertyName": "type" + } + } + ] + }, + "FieldBase": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the field.", + "readOnly": true, + "writeOnly": false + }, + "title": { + "type": "string", + "description": "Title of the field." + }, + "type": { + "type": "string", + "enum": [ + "SingleLineText", + "LongText", + "PhoneNumber", + "URL", + "Email", + "Number", + "Decimal", + "Currency", + "Percent", + "Duration", + "Date", + "DateTime", + "Time", + "SingleSelect", + "MultiSelect", + "Rating", + "Checkbox", + "Attachment", + "Geometry", + "Links", + "Lookup", + "Rollup", + "Button", + "Formula", + "Barcode", + "Year", + "QrCode", + "CreatedTime", + "LastModifiedTime", + "CreatedBy", + "LastModifiedBy", + "LinkToAnotherRecord", + "User", + "JSON" + ], + "description": "Field data type." + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "Description of the field." + }, + "default_value": { + "type": [ + "string", + "boolean", + "number" + ], + "description": "Default value for the field. Applicable for SingleLineText, LongText, PhoneNumber, URL, Email, Number, Decimal, Currency, Percent, Duration, Date, DateTime, Time, SingleSelect, MultiSelect, Rating, Checkbox, User and JSON fields." + } + }, + "required": [ + "title" + ] + }, + "FieldBaseCreate": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldBase" + }, + { + "required": [ + "title", + "type" + ] + } + ] + }, + "FieldOptions_LongText": { + "type": "object", + "title": "LongText", + "properties": { + "rich_text": { + "type": "boolean", + "description": "Enable rich text formatting." + }, + "generate_text_using_ai": { + "type": "boolean", + "description": "Enable text generation for this field using NocoAI." + } + }, + "additionalProperties": false + }, + "FieldOptions_PhoneNumber": { + "type": "object", + "title": "PhoneNumber", + "properties": { + "validation": { + "type": "boolean", + "description": "Enable validation for phone numbers." + } + }, + "additionalProperties": false + }, + "FieldOptions_URL": { + "type": "object", + "title": "URL", + "properties": { + "validation": { + "type": "boolean", + "description": "Enable validation for URL." + } + }, + "additionalProperties": false + }, + "FieldOptions_Email": { + "type": "object", + "title": "Email", + "properties": { + "validation": { + "type": "boolean", + "description": "Enable validation for Email." + } + }, + "additionalProperties": false + }, + "FieldOptions_Number": { + "type": "object", + "title": "Number", + "properties": { + "locale_string": { + "type": "boolean", + "description": "Show thousand separator on the UI." + } + }, + "additionalProperties": false + }, + "FieldOptions_Decimal": { + "type": "object", + "title": "Decimal", + "properties": { + "precision": { + "type": "number", + "description": "Decimal field precision. Defaults to 0", + "minimum": 0, + "maximum": 5 + } + }, + "additionalProperties": false + }, + "FieldOptions_Currency": { + "type": "object", + "title": "Currency", + "description": "Currency settings for this column. Locale defaults to `en-US` and currency code defaults to `USD`", + "properties": { + "locale": { + "type": "string", + "description": "Locale for currency formatting. Refer https://simplelocalize.io/data/locales/" + }, + "code": { + "type": "string", + "description": "Currency code. Refer https://simplelocalize.io/data/locales/", + "enum": [ + "AED", + "AFN", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BOV", + "BRL", + "BSD", + "BTN", + "BWP", + "BYR", + "BZD", + "CAD", + "CDF", + "CHE", + "CHF", + "CHW", + "CLF", + "CLP", + "CNY", + "COP", + "COU", + "CRC", + "CUP", + "CVE", + "CYP", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EEK", + "EGP", + "ERN", + "ETB", + "EUR", + "FJD", + "FKP", + "GBP", + "GEL", + "GHC", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HRK", + "HTG", + "HUF", + "IDR", + "ILS", + "INR", + "IQD", + "IRR", + "ISK", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KPW", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LTL", + "LVL", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRO", + "MTL", + "MUR", + "MVR", + "MWK", + "MXN", + "MXV", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "ROL", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SDD", + "SEK", + "SGD", + "SHP", + "SIT", + "SKK", + "SLL", + "SOS", + "SRD", + "STD", + "SYP", + "SZL", + "THB", + "TJS", + "TMM", + "TND", + "TOP", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "USN", + "USS", + "UYU", + "UZS", + "VEB", + "VND", + "VUV", + "WST", + "XAF", + "XAG", + "XAU", + "XBA", + "XBB", + "XBC", + "XBD", + "XCD", + "XDR", + "XFO", + "XFU", + "XOF", + "XPD", + "XPF", + "XPT", + "XTS", + "XXX", + "YER", + "ZAR", + "ZMK", + "ZWD" + ] + } + }, + "additionalProperties": false + }, + "FieldOptions_Percent": { + "type": "object", + "title": "Percent", + "properties": { + "show_as_progress": { + "type": "boolean", + "description": "Display as a progress bar." + } + }, + "additionalProperties": false + }, + "FieldOptions_Duration": { + "type": "object", + "title": "Duration", + "properties": { + "duration_format": { + "type": "string", + "description": "Duration format. Supported options are listed below\n- `h:mm`\n- `h:mm:ss`\n- `h:mm:ss.S`\n- `h:mm:ss.SS`\n- `h:mm:ss.SSS`" + } + }, + "additionalProperties": false + }, + "FieldOptions_DateTime": { + "type": "object", + "title": "DateTime", + "properties": { + "date_format": { + "type": "string", + "description": "Date format. Supported options are listed below\n- `YYYY/MM/DD`\n- `YYYY-MM-DD`\n- `YYYY MM DD`\n- `DD/MM/YYYY`\n- `DD-MM-YYYY`\n- `DD MM YYYY`\n- `MM/DD/YYYY`\n- `MM-DD-YYYY`\n- `MM DD YYYY`\n- `YYYY-MM`\n- `YYYY MM`" + }, + "time_format": { + "type": "string", + "description": "Time format. Supported options are listed below\n- `HH:mm`\n- `HH:mm:ss`\n- `HH:mm:ss.SSS`" + }, + "12hr_format": { + "type": "boolean", + "description": "Use 12-hour time format." + }, + "display_timezone": { + "type": "boolean", + "description": "Display timezone." + }, + "timezone": { + "type": "string", + "description": "Timezone. Refer to https://en.wikipedia.org/wiki/List_of_tz_database_time_zones" + }, + "use_same_timezone_for_all": { + "type": "boolean", + "description": "Use same timezone for all records." + } + }, + "additionalProperties": false + }, + "FieldOptions_Date": { + "type": "object", + "title": "Date", + "properties": { + "date_format": { + "type": "string", + "description": "Date format. Supported options are listed below\n- `YYYY/MM/DD`\n- `YYYY-MM-DD`\n- `YYYY MM DD`\n- `DD/MM/YYYY`\n- `DD-MM-YYYY`\n- `DD MM YYYY`\n- `MM/DD/YYYY`\n- `MM-DD-YYYY`\n- `MM DD YYYY`\n- `YYYY-MM`\n- `YYYY MM`" + } + }, + "additionalProperties": false + }, + "FieldOptions_Time": { + "type": "object", + "title": "Time", + "properties": { + "12hr_format": { + "type": "boolean", + "description": "Use 12-hour time format." + } + }, + "additionalProperties": false + }, + "FieldOptions_Select": { + "type": "object", + "title": "Single & MultiSelect", + "properties": { + "choices": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Choice title." + }, + "color": { + "type": "string", + "description": "Specifies the tile color for the choice using a hexadecimal color code (e.g., `#36BFFF`).", + "pattern": "^#[0-9A-Fa-f]{6}$" + } + }, + "required": [ + "title" + ] + }, + "uniqueItems": true + } + }, + "additionalProperties": false + }, + "FieldOptions_Rating": { + "type": "object", + "title": "Rating", + "properties": { + "icon": { + "type": "string", + "enum": [ + "star", + "heart", + "circle-filled", + "thumbs-up", + "flag" + ], + "description": "Icon to display rating on the UI. Supported options are listed below\n- `star`\n- `heart`\n- `circle-filled`\n- `thumbs-up`\n- `flag`" + }, + "max_value": { + "type": "integer", + "description": "Maximum value for the rating. Allowed range: 1-10.", + "minimum": 1, + "maximum": 10 + }, + "color": { + "type": "string", + "description": "Specifies icon color using a hexadecimal color code (e.g., `#36BFFF`).", + "pattern": "^#[0-9A-Fa-f]{6}$" + } + }, + "additionalProperties": false + }, + "FieldOptions_Checkbox": { + "type": "object", + "title": "Checkbox", + "properties": { + "icon": { + "type": "string", + "enum": [ + "square", + "circle-check", + "circle-filled", + "star", + "heart", + "thumbs-up", + "flag" + ], + "description": "Icon to display checkbox on the UI. Supported options are listed below\n- `square`\n- `circle-check`\n- `circle-filled`\n- `star`\n- `heart`\n- `thumbs-up`\n- `flag`" + }, + "color": { + "type": "string", + "description": "Specifies icon color using a hexadecimal color code (e.g., `#36BFFF`).", + "pattern": "^#[0-9A-Fa-f]{6}$" + } + }, + "additionalProperties": false + }, + "FieldOptions_Barcode": { + "type": "object", + "title": "Barcode", + "properties": { + "format": { + "type": "string", + "description": "Barcode format (e.g., CODE128)." + }, + "barcode_value_field_id": { + "type": "string", + "description": "Field ID that contains the value." + } + }, + "additionalProperties": false + }, + "FieldOptions_QrCode": { + "type": "object", + "title": "QrCode", + "properties": { + "qrcode_value_field_id": { + "type": "string", + "description": "Field ID that contains the value." + } + }, + "additionalProperties": false + }, + "FieldOptions_Formula": { + "type": "object", + "title": "Formula", + "properties": { + "formula": { + "type": "string", + "description": "Formula expression." + } + }, + "additionalProperties": false + }, + "FieldOptions_User": { + "type": "object", + "title": "User", + "properties": { + "allow_multiple_users": { + "type": "boolean", + "description": "Allow selecting multiple users." + } + }, + "additionalProperties": false + }, + "FieldOptions_Lookup": { + "type": "object", + "title": "Lookup", + "properties": { + "related_field_id": { + "type": "string", + "description": "Linked field ID. Can be of type Links or LinkToAnotherRecord" + }, + "related_table_lookup_field_id": { + "type": "string", + "description": "Lookup field ID in the linked table." + } + }, + "required": [ + "related_field_id", + "related_table_lookup_field_id" + ], + "additionalProperties": false + }, + "FieldOptions_Rollup": { + "type": "object", + "title": "Rollup", + "properties": { + "related_field_id": { + "type": "string", + "description": "Linked field ID." + }, + "related_table_rollup_field_id": { + "type": "string", + "description": "Rollup field ID in the linked table." + }, + "rollup_function": { + "type": "string", + "description": "Rollup function.", + "enum": [ + "count", + "min", + "max", + "avg", + "sum", + "countDistinct", + "sumDistinct", + "avgDistinct" + ] + } + }, + "required": [ + "related_field_id", + "related_table_rollup_field_id", + "rollup_function" + ], + "additionalProperties": false + }, + "FieldOptions_Button": { + "type": "object", + "title": "Button", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "formula" + ], + "description": "Button type: formula" + }, + "formula": { + "type": "string", + "description": "Formula to execute" + } + }, + "required": [ + "type", + "formula" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "webhook" + ], + "description": "Button type: webhook" + }, + "button_hook_id": { + "type": "string", + "description": "ID of the webhook to trigger" + } + }, + "required": [ + "type", + "button_hook_id" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ai" + ], + "description": "Button type: AI" + }, + "prompt": { + "type": "string", + "description": "AI prompt to execute" + }, + "integration_id": { + "type": "string", + "description": "Integration ID for AI service" + }, + "theme": { + "type": "string", + "description": "Theme of the button" + }, + "output_column_ids": { + "type": "string", + "description": "IDs of columns where AI output should be stored" + }, + "label": { + "type": "string", + "description": "Label of the button" + }, + "icon": { + "type": "string", + "description": "Icon of the button" + }, + "color": { + "type": "string", + "description": "Color of the button" + } + }, + "required": [ + "type", + "prompt", + "integration_id" + ] + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "formula": "#/components/schemas/0", + "webhook": "#/components/schemas/1", + "ai": "#/components/schemas/2" + } + }, + "additionalProperties": false + }, + "FieldOptions_Links": { + "type": "object", + "title": "Links", + "properties": { + "relation_type": { + "type": "string", + "description": "Type of relationship.\n\nSupported options are listed below\n- `mm` many-to-many\n- `hm` has-many\n- `oo` one-to-one" + }, + "related_table_id": { + "type": "string", + "description": "Identifier of the linked table." + } + }, + "required": [ + "relation_type", + "related_table_id" + ], + "additionalProperties": false + }, + "FieldOptions_LinkToAnotherRecord": { + "type": "object", + "title": "LinkToAnotherRecord", + "properties": { + "relation_type": { + "type": "string", + "description": "Type of relationship.\n\nSupported options are listed below\n- `mm` many-to-many\n- `hm` has-many\n- `oo` one-to-one" + }, + "related_table_id": { + "type": "string", + "description": "Identifier of the linked table." + } + }, + "required": [ + "relation_type", + "related_table_id" + ], + "additionalProperties": false + }, + "Field": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldBase" + }, + { + "oneOf": [ + { + "properties": { + "type": { + "enum": [ + "SingleLineText" + ] + } + } + }, + { + "properties": { + "type": { + "enum": [ + "LongText" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_LongText" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "PhoneNumber", + "URL", + "Email" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_PhoneNumber" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "Number", + "Decimal" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Number" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "JSON" + ] + } + } + }, + { + "properties": { + "type": { + "enum": [ + "Currency" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Currency" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "Percent" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Percent" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "Duration" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Duration" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "Date", + "DateTime", + "Time" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_DateTime" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "SingleSelect", + "MultiSelect" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Select" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "Rating", + "Checkbox" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Rating" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "Barcode" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Barcode" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "Formula" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Formula" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "User" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_User" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "Lookup" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Lookup" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "Links" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Links" + } + } + }, + { + "properties": { + "type": { + "enum": [ + "LinkToAnotherRecord" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_LinkToAnotherRecord" + } + } + } + ] + } + ] + }, + "FilterCreateUpdate": { + "oneOf": [ + { + "$ref": "#/components/schemas/Filter" + }, + { + "$ref": "#/components/schemas/FilterGroup" + } + ] + }, + "FieldUpdate": { + "allOf": [ + { + "$ref": "#/components/schemas/FieldBase" + }, + { + "oneOf": [ + { + "$ref": "#/components/schemas/FieldOptions/SingleLineText" + }, + { + "title": "LongText", + "properties": { + "type": { + "enum": [ + "LongText" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_LongText" + } + } + }, + { + "title": "PhoneNumber", + "properties": { + "type": { + "enum": [ + "PhoneNumber", + "URL", + "Email" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_PhoneNumber" + } + } + }, + { + "title": "Number / Decimal", + "properties": { + "type": { + "enum": [ + "Number", + "Decimal" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Number" + } + } + }, + { + "title": "JSON", + "properties": { + "type": { + "enum": [ + "JSON" + ] + } + } + }, + { + "title": "Currency", + "properties": { + "type": { + "enum": [ + "Currency" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Currency" + } + } + }, + { + "title": "Percent", + "properties": { + "type": { + "enum": [ + "Percent" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Percent" + } + } + }, + { + "title": "Duration", + "properties": { + "type": { + "enum": [ + "Duration" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Duration" + } + } + }, + { + "title": "Date / DateTime", + "properties": { + "type": { + "enum": [ + "Date", + "DateTime", + "Time" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_DateTime" + } + } + }, + { + "title": "Single / MultiSelect", + "properties": { + "type": { + "enum": [ + "SingleSelect", + "MultiSelect" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Select" + } + } + }, + { + "title": "Checkbox", + "properties": { + "type": { + "enum": [ + "Checkbox" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Checkbox" + } + } + }, + { + "title": "Rating", + "properties": { + "type": { + "enum": [ + "Rating" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Rating" + } + } + }, + { + "title": "Barcode", + "properties": { + "type": { + "enum": [ + "Barcode" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Barcode" + } + } + }, + { + "title": "Formula", + "properties": { + "type": { + "enum": [ + "Formula" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Formula" + } + } + }, + { + "title": "User", + "properties": { + "type": { + "enum": [ + "User" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_User" + } + } + }, + { + "title": "Lookup", + "properties": { + "type": { + "enum": [ + "Lookup" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Lookup" + } + } + }, + { + "title": "Links", + "properties": { + "type": { + "enum": [ + "Links" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_Links" + } + } + }, + { + "title": "LinkToAnotherRecord", + "properties": { + "type": { + "enum": [ + "LinkToAnotherRecord" + ] + }, + "options": { + "$ref": "#/components/schemas/FieldOptions_LinkToAnotherRecord" + } + } + } + ] + } + ] + }, + "Filter": { + "type": "object", + "title": "Filter", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the filter.", + "readOnly": true + }, + "parent_id": { + "type": "string", + "description": "Parent ID of the filter, specifying this filters group association. Defaults to **root**." + }, + "field_id": { + "type": "string", + "description": "Field ID to which this filter applies." + }, + "operator": { + "type": "string", + "description": "Primary comparison operator (e.g., eq, gt, lt)." + }, + "sub_operator": { + "type": [ + "string", + "null" + ], + "description": "Secondary comparison operator (if applicable)." + }, + "value": { + "type": [ + "string", + "number", + "boolean", + "null" + ], + "description": "Value for comparison." + } + }, + "required": [ + "field_id", + "operator", + "value" + ] + }, + "FilterListResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FilterGroup" + }, + "description": "List of filter groups. Initial set of filters are mapped to a default group with group-id set to **root**." + } + }, + "required": [ + "list" + ] + }, + "FilterGroupLevel3": { + "type": "object", + "properties": { + "group_operator": { + "type": "string", + "enum": [ + "AND", + "OR" + ], + "description": "Logical operator for the group." + }, + "filters": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Filter" + }, + "description": "List of filters in this group." + } + }, + "required": [ + "group_operator", + "filters" + ] + }, + "FilterGroupLevel2": { + "type": "object", + "properties": { + "group_operator": { + "type": "string", + "enum": [ + "AND", + "OR" + ], + "description": "Logical operator for the group." + }, + "filters": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/Filter" + }, + { + "$ref": "#/components/schemas/FilterGroupLevel3" + } + ] + }, + "description": "List of filters or nested filter groups at level 3." + } + }, + "required": [ + "group_operator", + "filters" + ] + }, + "FilterGroupLevel1": { + "type": "object", + "properties": { + "group_operator": { + "type": "string", + "enum": [ + "AND", + "OR" + ], + "description": "Logical operator for the group." + }, + "filters": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/Filter" + }, + { + "$ref": "#/components/schemas/FilterGroupLevel2" + } + ] + }, + "description": "List of filters or nested filter groups at level 2." + } + }, + "required": [ + "group_operator", + "filters" + ] + }, + "FilterGroup": { + "type": "object", + "title": "FilterGroup", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the group.", + "readOnly": true + }, + "parent_id": { + "type": "string", + "description": "Parent ID of this filter-group." + }, + "group_operator": { + "type": "string", + "enum": [ + "AND", + "OR" + ], + "description": "Logical operator for combining filters in the group." + }, + "filters": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/Filter" + }, + { + "$ref": "#/components/schemas/FilterGroup" + } + ] + }, + "description": "Nested filters or filter groups." + } + }, + "required": [ + "id", + "group_operator", + "filters" + ] + }, + "FilterCreate": { + "oneOf": [ + { + "$ref": "#/components/schemas/Filter" + }, + { + "$ref": "#/components/schemas/FilterGroupLevel1" + } + ] + }, + "FilterUpdate": { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the filter." + } + }, + "required": [ + "id" + ] + }, + { + "oneOf": [ + { + "$ref": "#/components/schemas/Filter" + }, + { + "$ref": "#/components/schemas/FilterGroup" + } + ] + } + ] + }, + "SortListResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Sort" + } + } + }, + "required": [ + "list" + ] + }, + "DataRecordV3": { + "type": "object", + "description": "V3 Data Record format with id and fields separation", + "properties": { + "id": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "description": "Record identifier (primary key value)" + }, + "fields": { + "type": "object", + "description": "Record fields data (excluding primary key). Undefined when empty.", + "additionalProperties": true + } + }, + "required": [ + "id" + ] + }, + "DataRecordWithDeletedV3": { + "allOf": [ + { + "$ref": "#/components/schemas/DataRecordV3" + }, + { + "type": "object", + "properties": { + "deleted": { + "type": "boolean", + "description": "Indicates if the record was deleted" + } + }, + "required": [ + "deleted" + ] + } + ] + }, + "DataListResponseV3": { + "type": "object", + "description": "V3 Data List Response format", + "properties": { + "records": { + "type": "array", + "description": "Array of records for has-many and many-to-many relationships", + "items": { + "$ref": "#/components/schemas/DataRecordV3" + } + }, + "next": { + "type": [ + "string", + "null" + ], + "description": "Pagination token for next page" + }, + "prev": { + "type": [ + "string", + "null" + ], + "description": "Pagination token for previous page" + }, + "nestedNext": { + "type": [ + "string", + "null" + ], + "description": "Nested pagination token for next page" + }, + "nestedPrev": { + "type": [ + "string", + "null" + ], + "description": "Nested pagination token for previous page" + } + } + }, + "DataInsertRequestV3": { + "type": "object", + "description": "V3 Data Insert Request format", + "properties": { + "fields": { + "type": "object", + "description": "Record fields data", + "additionalProperties": true + } + }, + "required": [ + "fields" + ] + }, + "DataUpdateRequestV3": { + "type": "object", + "description": "V3 Data Update Request format", + "properties": { + "id": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "description": "Record identifier" + }, + "fields": { + "type": "object", + "description": "Record fields data to update", + "additionalProperties": true + } + }, + "required": [ + "id", + "fields" + ] + }, + "DataDeleteRequestV3": { + "type": "object", + "description": "Single record delete request", + "properties": { + "id": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "description": "Record identifier" + } + }, + "required": [ + "id" + ] + }, + "DataInsertResponseV3": { + "type": "object", + "description": "V3 Data Insert Response format", + "properties": { + "records": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DataRecordV3" + }, + "description": "Array of created records" + } + }, + "required": [ + "records" + ] + }, + "DataUpdateResponseV3": { + "type": "object", + "description": "V3 Data Update Response format", + "properties": { + "records": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "description": "Updated record identifier" + }, + "fields": { + "type": "object", + "description": "Record fields data (excluding primary key). Undefined when empty.", + "additionalProperties": true + } + }, + "required": [ + "id" + ] + }, + "description": "Array of updated record identifiers" + } + }, + "required": [ + "records" + ] + }, + "DataDeleteResponseV3": { + "type": "object", + "description": "V3 Data Delete Response format", + "properties": { + "records": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DataRecordWithDeletedV3" + }, + "description": "Array of deleted records" + } + }, + "required": [ + "records" + ] + }, + "DataReadResponseV3": { + "$ref": "#/components/schemas/DataRecordV3", + "description": "V3 Data Read Response format" + }, + "DataNestedListResponseV3": { + "type": "object", + "description": "V3 Nested Data List Response format - supports both single record and array responses", + "properties": { + "records": { + "type": "array", + "description": "Array of records for has-many and many-to-many relationships", + "items": { + "$ref": "#/components/schemas/DataRecordV3" + } + }, + "record": { + "description": "Single record for belongs-to and one-to-one relationships", + "oneOf": [ + { + "$ref": "#/components/schemas/DataRecordV3" + }, + { + "type": "null" + } + ] + }, + "next": { + "type": [ + "string", + "null" + ], + "description": "Pagination token for next page" + }, + "prev": { + "type": [ + "string", + "null" + ], + "description": "Pagination token for previous page" + } + } + }, + "Paginated": { + "description": "Model for Paginated", + "examples": [ + { + "next": "http://api.nocodd.com/api/v3/tables/id?page=3", + "prev": "http://api.nocodd.com/api/v3/tables/id?page=1" + } + ], + "properties": { + "next": { + "description": "URL to access next page", + "type": "string" + }, + "prev": { + "description": "URL to access previous page", + "type": "string" + }, + "nestedNext": { + "description": "URL to access current page data with next set of nested fields data", + "type": "string" + }, + "nestedPrev": { + "description": "URL to access current page data with previous set of nested fields data", + "type": "string" + } + }, + "title": "Paginated Model", + "type": "object" + }, + "BaseRoles": { + "type": "string", + "description": "Base roles for the user.", + "enum": [ + "owner", + "creator", + "editor", + "viewer", + "commenter", + "no-access" + ] + }, + "Workspace": { + "type": "object", + "description": "Basic workspace information", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the workspace" + }, + "title": { + "type": "string", + "description": "Title of the workspace" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the workspace was created" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the workspace was last updated" + } + }, + "required": [ + "id", + "title", + "created_at", + "updated_at" + ] + }, + "WorkspaceWithMembers": { + "type": "object", + "description": "Workspace information including member details", + "allOf": [ + { + "$ref": "#/components/schemas/Workspace" + }, + { + "type": "object", + "properties": { + "individual_members": { + "type": "object", + "properties": { + "workspace_members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkspaceMember" + }, + "description": "List of workspace members" + } + }, + "required": [ + "workspace_members" + ] + } + }, + "required": [ + "individual_members" + ] + } + ] + }, + "WorkspaceMember": { + "type": "object", + "description": "Individual workspace member information", + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "Email address of the member" + }, + "user_id": { + "type": "string", + "description": "Unique identifier for the user" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the user was added to the workspace" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the user was last updated in the workspace" + }, + "workspace_role": { + "$ref": "#/components/schemas/WorkspaceRoles", + "description": "Role assigned to the user in the workspace" + } + }, + "required": [ + "email", + "user_id", + "created_at", + "updated_at", + "workspace_role" + ] + }, + "WorkspaceUser": { + "type": "object", + "description": "Workspace user information", + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "Email address of the user" + }, + "user_id": { + "type": "string", + "description": "Unique identifier for the user" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the user was added to the workspace" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the user was last updated in the workspace" + }, + "workspace_role": { + "$ref": "#/components/schemas/WorkspaceRoles", + "description": "Role assigned to the user in the workspace" + } + }, + "required": [ + "email", + "user_id", + "created_at", + "updated_at", + "workspace_role" + ] + }, + "WorkspaceUserCreate": { + "type": "array", + "items": { + "type": "object", + "oneOf": [ + { + "title": "Invite User with ID", + "required": [ + "user_id", + "workspace_role" + ], + "properties": { + "user_id": { + "type": "string", + "description": "Unique identifier for the user (skip if email is provided)" + }, + "workspace_role": { + "$ref": "#/components/schemas/WorkspaceRoles", + "description": "Workspace role to assign to the user" + } + } + }, + { + "title": "Invite User with Email", + "required": [ + "email", + "workspace_role" + ], + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "Email address of the user (skip if user_id is provided)" + }, + "workspace_role": { + "$ref": "#/components/schemas/WorkspaceRoles", + "description": "Workspace role to assign to the user" + } + } + } + ], + "description": "An object representing a new workspace user to be created." + }, + "description": "Array of workspace users to be created." + }, + "WorkspaceUserUpdate": { + "type": "array", + "items": { + "type": "object", + "properties": { + "user_id": { + "type": "string", + "description": "Unique identifier for the user" + }, + "workspace_role": { + "$ref": "#/components/schemas/WorkspaceRoles", + "description": "New workspace role to assign to the user" + } + }, + "required": [ + "user_id", + "workspace_role" + ], + "description": "An object representing updates for an existing workspace user." + }, + "description": "Array of workspace user updates." + }, + "WorkspaceUserDelete": { + "type": "array", + "items": { + "type": "object", + "properties": { + "user_id": { + "type": "string", + "description": "Unique identifier for the user" + } + }, + "required": [ + "user_id" + ], + "description": "An object representing a workspace user to be deleted." + }, + "description": "Array of workspace users to be deleted." + }, + "WorkspaceRoles": { + "type": "string", + "description": "Workspace roles for the user.", + "enum": [ + "workspace-level-owner", + "workspace-level-creator", + "workspace-level-editor", + "workspace-level-viewer", + "workspace-level-commenter", + "workspace-level-no-access" + ] + } + }, + "responses": { + "BadRequest": { + "description": "BadRequest", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "msg": { + "type": "string", + "x-stoplight": { + "id": "p9mk4oi0hbihm" + }, + "example": "BadRequest [Error]: " + } + }, + "required": [ + "msg" + ] + }, + "examples": { + "Example 1": { + "value": { + "msg": "BadRequest [Error]: " + } + } + } + } + }, + "headers": {} + } + }, + "securitySchemes": { + "xc-token": { + "name": "Auth Token ", + "type": "apiKey", + "in": "header", + "description": "Auth Token is a JWT Token generated based on the logged-in user. By default, the token is only valid for 10 hours. However, you can change the value by defining it using environment variable `NC_JWT_EXPIRES_IN`." + }, + "bearerAuth": { + "name": "Authorization", + "type": "http", + "scheme": "bearer", + "description": "Bearer token authentication. Use 'Authorization: Bearer ' header format. This is an alternative to the xc-token header." + }, + "xc-shared-base-id": { + "name": "Shared Base ID", + "type": "apiKey", + "in": "header", + "description": "Shared base uuid" + }, + "xc-shared-erd-id": { + "name": "Shared ERD ID", + "type": "apiKey", + "in": "header", + "description": "Shared ERD uuid" + } + }, + "parameters": { + "xc-token": { + "name": "xc-token", + "in": "header", + "required": true, + "schema": { + "type": "string" + }, + "description": "API Token. Refer [here](https://docs.nocodb.com/account-settings/api-tokens/) to know more" + } + } + } +} From dadf3a214ccba58f74330e25fa4293ec7899b43c Mon Sep 17 00:00:00 2001 From: Karl Bauer Date: Fri, 10 Oct 2025 11:47:05 +0200 Subject: [PATCH 3/7] chore: Add scripts for comprehensive API comparison and schema analysis between NocoDB v2 and v3 - Implement `analyze_api_diff.py` to compare endpoints, parameters, request/response schemas, and generate a markdown report summarizing the differences. - Implement `analyze_schemas.py` to analyze request/response schemas and query parameters, generating a detailed report on schema changes and observations. --- docs/API_COMPARISON_README.md | 226 +++++ docs/API_COMPARISON_V2_V3.md | 506 ++++++++++++ docs/API_V2_V3_MIGRATION_GUIDE.md | 644 +++++++++++++++ docs/NOCODB_API_SCHEMA_COMPARISON.md | 176 ++++ docs/NOCODB_API_V2_V3_COMPARISON.md | 618 ++++++++++++++ scripts/migration/analyze_api_detailed.py | 958 ++++++++++++++++++++++ scripts/migration/analyze_api_diff.py | 417 ++++++++++ scripts/migration/analyze_schemas.py | 355 ++++++++ 8 files changed, 3900 insertions(+) create mode 100644 docs/API_COMPARISON_README.md create mode 100644 docs/API_COMPARISON_V2_V3.md create mode 100644 docs/API_V2_V3_MIGRATION_GUIDE.md create mode 100644 docs/NOCODB_API_SCHEMA_COMPARISON.md create mode 100644 docs/NOCODB_API_V2_V3_COMPARISON.md create mode 100644 scripts/migration/analyze_api_detailed.py create mode 100644 scripts/migration/analyze_api_diff.py create mode 100644 scripts/migration/analyze_schemas.py diff --git a/docs/API_COMPARISON_README.md b/docs/API_COMPARISON_README.md new file mode 100644 index 0000000..5cd409e --- /dev/null +++ b/docs/API_COMPARISON_README.md @@ -0,0 +1,226 @@ +# NocoDB API v2 to v3 Comparison Documentation + +This directory contains comprehensive analysis and migration guides for the NocoDB API v2 to v3 transition. + +## 📚 Documentation Index + +### 1. **Quick Start: Migration Guide** ⭐ +**File:** [`API_V2_V3_MIGRATION_GUIDE.md`](./API_V2_V3_MIGRATION_GUIDE.md) + +**Use this if you need:** Practical migration steps, code examples, and quick reference tables. + +**Contents:** +- Critical breaking changes by priority +- Code migration examples for most-used endpoints +- Implementation strategies (adapter pattern, query parameter conversion) +- Base ID resolution strategies +- Testing checklist +- Common pitfalls and solutions +- Quick reference tables +- Timeline estimates and risk assessment + +**Best for:** Developers actively migrating code from v2 to v3. + +--- + +### 2. **Comprehensive Comparison Report** +**File:** [`NOCODB_API_V2_V3_COMPARISON.md`](./NOCODB_API_V2_V3_COMPARISON.md) + +**Use this if you need:** Deep understanding of all architectural changes and endpoint differences. + +**Contents:** +- Executive summary with statistics +- Major architectural changes explained +- Complete list of removed endpoints (137 from Meta API) +- Complete list of new endpoints (36 in Data API) +- Critical breaking changes tables +- Migration path analysis +- Detailed implementation strategy +- Version detection code +- Base ID resolver implementation + +**Best for:** Architects planning the migration strategy and understanding scope. + +--- + +### 3. **Schema & Parameter Analysis** +**File:** [`NOCODB_API_SCHEMA_COMPARISON.md`](./NOCODB_API_SCHEMA_COMPARISON.md) + +**Use this if you need:** Detailed query parameter and response schema changes. + +**Contents:** +- Query parameter comparison for list records +- Pagination changes (offset/limit → page/pageSize) +- Sort format changes (string → JSON) +- Response schema structures +- Error response format differences +- Field naming conventions + +**Best for:** Developers implementing query parameter conversion and response parsing. + +--- + +### 4. **Initial Comparison Report** +**File:** [`API_COMPARISON_V2_V3.md`](./API_COMPARISON_V2_V3.md) + +**Use this if you need:** High-level overview and endpoint categorization. + +**Contents:** +- Statistics (endpoints removed/added) +- Endpoints categorized by type (Table, Record, View, etc.) +- Basic breaking changes summary +- Simple implementation recommendations + +**Best for:** Initial assessment and presenting to stakeholders. + +--- + +## 🚀 Quick Navigation by Use Case + +### "I need to understand what changed" +→ Start with [`NOCODB_API_V2_V3_COMPARISON.md`](./NOCODB_API_V2_V3_COMPARISON.md) (Section: Executive Summary) + +### "I need to migrate record operations" +→ Go to [`API_V2_V3_MIGRATION_GUIDE.md`](./API_V2_V3_MIGRATION_GUIDE.md) (Section: Most Used Endpoints) + +### "I need to understand pagination changes" +→ Go to [`NOCODB_API_SCHEMA_COMPARISON.md`](./NOCODB_API_SCHEMA_COMPARISON.md) (Section: List Records Query Parameters) + +### "I need implementation code examples" +→ Go to [`API_V2_V3_MIGRATION_GUIDE.md`](./API_V2_V3_MIGRATION_GUIDE.md) (Section: Implementation Strategy) + +### "I need to handle baseId resolution" +→ Go to [`API_V2_V3_MIGRATION_GUIDE.md`](./API_V2_V3_MIGRATION_GUIDE.md) (Section: Base ID Resolution) + +### "I need a complete list of endpoint changes" +→ Go to [`NOCODB_API_V2_V3_COMPARISON.md`](./NOCODB_API_V2_V3_COMPARISON.md) (Sections: Removed/New Endpoints) + +--- + +## ⚠️ Critical Findings Summary + +### 1. API File Role Reversal +**v2 and v3 have INVERTED their API file definitions!** + +- v2's "Meta API" (137 endpoints) = v3's "Data API" (36 endpoints) +- v2's "Data API" (10 endpoints) = v3's "Meta API" (10 endpoints) + +This is NOT just naming - you must load the opposite file for equivalent operations. + +### 2. Base ID Now Required +**100% of endpoints now require `baseId` in the path.** + +```diff +- /api/v2/tables/{tableId}/records ++ /api/v3/data/{baseId}/{tableId}/records +``` + +### 3. Pagination Redesigned +**Complete breaking change in pagination.** + +| v2 | v3 | +|----|-----| +| `offset=50&limit=25` | `page=3&pageSize=25` | + +### 4. Sort Format Changed +**String format replaced with JSON.** + +```diff +- sort=field1,-field2 ++ sort=[{"direction":"asc","field":"field1"},{"direction":"desc","field":"field2"}] +``` + +### 5. Terminology Changes +- `columns` → `fields` +- `columnId` → `fieldId` +- `ne` operator → `neq` operator + +--- + +## 📊 Statistics + +| Metric | v2 | v3 | Change | +|--------|----|----|--------| +| **Meta API Endpoints** | 137 | 10 | -93% | +| **Data API Endpoints** | 10 | 36 | +260% | +| **Total Endpoints** | 147 | 46 | -69% | +| **Breaking Changes** | - | 147 | 100% | + +--- + +## 🔍 Source OpenAPI Files + +All analysis is based on these OpenAPI specification files: + +- **v2 Meta API:** `docs/nocodb-openapi-meta.json` (137 endpoints) +- **v2 Data API:** `docs/nocodb-openapi-data.json` (10 endpoints) +- **v3 Meta API:** `docs/nocodb-openapi-meta-v3.json` (10 endpoints) +- **v3 Data API:** `docs/nocodb-openapi-data-v3.json` (36 endpoints) + +--- + +## 🛠️ Analysis Scripts + +The following Python scripts were used to generate these reports: + +1. **`analyze_api_diff.py`** - Basic endpoint comparison +2. **`analyze_api_detailed.py`** - Comprehensive analysis with migration examples +3. **`analyze_schemas.py`** - Query parameter and schema analysis + +--- + +## ⏱️ Estimated Migration Timeline + +| Phase | Duration | +|-------|----------| +| Analysis & Planning | 1-2 weeks | +| Implementation | 2-3 weeks | +| Testing | 2 weeks | +| Deployment | 1 week | +| **Total** | **6-8 weeks** | + +--- + +## 🎯 Recommended Reading Order + +### For Developers: +1. [`API_V2_V3_MIGRATION_GUIDE.md`](./API_V2_V3_MIGRATION_GUIDE.md) - Start here +2. [`NOCODB_API_SCHEMA_COMPARISON.md`](./NOCODB_API_SCHEMA_COMPARISON.md) - For parameter details +3. [`NOCODB_API_V2_V3_COMPARISON.md`](./NOCODB_API_V2_V3_COMPARISON.md) - For complete reference + +### For Architects: +1. [`NOCODB_API_V2_V3_COMPARISON.md`](./NOCODB_API_V2_V3_COMPARISON.md) - Start here +2. [`API_V2_V3_MIGRATION_GUIDE.md`](./API_V2_V3_MIGRATION_GUIDE.md) - For implementation strategy +3. [`API_COMPARISON_V2_V3.md`](./API_COMPARISON_V2_V3.md) - For stakeholder presentation + +### For QA/Testing: +1. [`API_V2_V3_MIGRATION_GUIDE.md`](./API_V2_V3_MIGRATION_GUIDE.md) - Testing checklist section +2. [`NOCODB_API_SCHEMA_COMPARISON.md`](./NOCODB_API_SCHEMA_COMPARISON.md) - Parameter validation + +--- + +## 📝 Document Versions + +All documents were generated on: **2025-10-10** + +Based on OpenAPI specifications from the `docs/` directory. + +--- + +## 🤝 Contributing + +If you find discrepancies or need additional analysis: +1. Check the source OpenAPI files first +2. Run the analysis scripts to regenerate reports +3. Submit findings with specific endpoint examples + +--- + +## ⚖️ License + +These documentation files are part of the NocoDB_SimpleClient project. + +--- + +**Quick Links:** +- [Migration Guide](./API_V2_V3_MIGRATION_GUIDE.md) | [Full Comparison](./NOCODB_API_V2_V3_COMPARISON.md) | [Schema Analysis](./NOCODB_API_SCHEMA_COMPARISON.md) diff --git a/docs/API_COMPARISON_V2_V3.md b/docs/API_COMPARISON_V2_V3.md new file mode 100644 index 0000000..5e378fe --- /dev/null +++ b/docs/API_COMPARISON_V2_V3.md @@ -0,0 +1,506 @@ +# NocoDB API v2 to v3 Comprehensive Comparison Report + +**Generated:** 2025-10-10 11:21:10 + +## Executive Summary + +### Meta API Changes +- **Removed Endpoints:** 137 +- **New Endpoints:** 10 +- **Potentially Renamed:** 0 +- **Unchanged:** 0 + +### Data API Changes +- **Removed Endpoints:** 10 +- **New Endpoints:** 36 +- **Potentially Renamed:** 0 +- **Unchanged:** 0 + +--- + +## Meta API: v2 → v3 Differences + +### Removed Endpoints + +#### Authentication + +| Method | Path | Summary | +|--------|------|---------| +| `GET` | `/api/v2/auth/user/me` | Get User Info | +| `POST` | `/api/v2/auth/email/validate/{token}` | Verify Email | +| `POST` | `/api/v2/auth/password/change` | Change Password | +| `POST` | `/api/v2/auth/password/forgot` | Forget Password | +| `POST` | `/api/v2/auth/password/reset/{token}` | Reset Password | +| `POST` | `/api/v2/auth/token/refresh` | Refresh Token | +| `POST` | `/api/v2/auth/token/validate/{token}` | Verify Reset Token | +| `POST` | `/api/v2/auth/user/signin` | Signin | +| `POST` | `/api/v2/auth/user/signout` | Signout | +| `POST` | `/api/v2/auth/user/signup` | Signup | + +#### Column/Field Operations + +| Method | Path | Summary | +|--------|------|---------| +| `DELETE` | `/api/v2/meta/columns/{columnId}` | Delete Column | +| `GET` | `/api/v2/meta/columns/{columnId}` | Get Column Metadata | +| `GET` | `/api/v2/meta/views/{viewId}/columns` | List View Columns | +| `PATCH` | `/api/v2/meta/columns/{columnId}` | Update Column | +| `PATCH` | `/api/v2/meta/views/{viewId}/columns/{columnId}` | Update View Column | +| `POST` | `/api/v2/meta/columns/{columnId}/primary` | Create Primary Value | +| `POST` | `/api/v2/meta/views/{viewId}/columns` | Create Column in View | + +#### File Operations + +| Method | Path | Summary | +|--------|------|---------| +| `POST` | `/api/v2/storage/upload` | Attachment Upload | + +#### Meta Operations + +| Method | Path | Summary | +|--------|------|---------| +| `DELETE` | `/api/v2/meta/bases/{baseId}` | Delete Base | +| `DELETE` | `/api/v2/meta/bases/{baseId}/api-tokens/{tokenId}` | Delete API Token | +| `DELETE` | `/api/v2/meta/bases/{baseId}/shared` | Delete Base Shared Base | +| `DELETE` | `/api/v2/meta/bases/{baseId}/sources/{sourceId}` | Delete Source | +| `DELETE` | `/api/v2/meta/bases/{baseId}/sources/{sourceId}/share/erd` | | +| `DELETE` | `/api/v2/meta/bases/{baseId}/users/{userId}` | Delete Base User | +| `DELETE` | `/api/v2/meta/cache` | Delete Cache | +| `DELETE` | `/api/v2/meta/comment/{commentId}` | Delete Comment | +| `DELETE` | `/api/v2/meta/filters/{filterId}` | Delete Filter | +| `DELETE` | `/api/v2/meta/sorts/{sortId}` | Delete Sort | +| `DELETE` | `/api/v2/meta/workspaces/{workspaceId}` | Delete workspace ☁ | +| `DELETE` | `/api/v2/meta/workspaces/{workspaceId}/users/{userId}` | Delete workspace user ☁ | +| `GET` | `/api/v2/meta/bases/` | List Bases (OSS) | +| `GET` | `/api/v2/meta/bases/{baseId}` | Get Base Schema | +| `GET` | `/api/v2/meta/bases/{baseId}/api-tokens` | List API Tokens in Base | +| `GET` | `/api/v2/meta/bases/{baseId}/cost` | Base Cost | +| `GET` | `/api/v2/meta/bases/{baseId}/has-empty-or-null-filters` | List Empty & Null Filter | +| `GET` | `/api/v2/meta/bases/{baseId}/info` | Get Base info | +| `GET` | `/api/v2/meta/bases/{baseId}/meta-diff` | Meta Diff | +| `GET` | `/api/v2/meta/bases/{baseId}/meta-diff/{sourceId}` | Source Meta Diff | +| `GET` | `/api/v2/meta/bases/{baseId}/shared` | Get Base Shared Base | +| `GET` | `/api/v2/meta/bases/{baseId}/sources/` | List Sources | +| `GET` | `/api/v2/meta/bases/{baseId}/sources/{sourceId}` | Get Source Schema | +| `GET` | `/api/v2/meta/bases/{baseId}/users` | List Base Users | +| `GET` | `/api/v2/meta/bases/{baseId}/visibility-rules` | Get UI ACL | +| `GET` | `/api/v2/meta/cache` | Get Cache | +| `GET` | `/api/v2/meta/comments` | List Comments | +| `GET` | `/api/v2/meta/comments/count` | Count Comments | +| `GET` | `/api/v2/meta/filters/{filterGroupId}/children` | Get Filter Group Children | +| `GET` | `/api/v2/meta/filters/{filterId}` | Get Filter Metadata | +| `GET` | `/api/v2/meta/forms/{formViewId}` | Get Form View Metadata | +| `GET` | `/api/v2/meta/galleries/{galleryViewId}` | Get Gallery View Metadata | +| `GET` | `/api/v2/meta/grids/{gridId}/grid-columns` | List Grid View Columns | +| `GET` | `/api/v2/meta/kanbans/{kanbanViewId}` | Get Kanban View Metadata | +| `GET` | `/api/v2/meta/maps/{mapViewId}` | Get Map View | +| `GET` | `/api/v2/meta/nocodb/info` | Get App Info | +| `GET` | `/api/v2/meta/sorts/{sortId}` | Get Sort Metadata | +| `GET` | `/api/v2/meta/workspaces` | List workspaces ☁ | +| `GET` | `/api/v2/meta/workspaces/{workspaceId}` | Read workspace ☁ | +| `GET` | `/api/v2/meta/workspaces/{workspaceId}/bases` | List Bases | +| `GET` | `/api/v2/meta/workspaces/{workspaceId}/users` | Workspace users list ☁ | +| `GET` | `/api/v2/meta/workspaces/{workspaceId}/users/{userId}` | Workspace user read ☁ | +| `PATCH` | `/api/v2/meta/bases/{baseId}` | Update Base | +| `PATCH` | `/api/v2/meta/bases/{baseId}/shared` | Update Base Shared Base | +| `PATCH` | `/api/v2/meta/bases/{baseId}/sources/{sourceId}` | Update Source | +| `PATCH` | `/api/v2/meta/bases/{baseId}/user` | Base user meta update | +| `PATCH` | `/api/v2/meta/bases/{baseId}/users/{userId}` | Update Base User | +| `PATCH` | `/api/v2/meta/comment/{commentId}` | Update Comment | +| `PATCH` | `/api/v2/meta/filters/{filterId}` | Update Filter | +| `PATCH` | `/api/v2/meta/form-columns/{formViewColumnId}` | Update Form View Column | +| `PATCH` | `/api/v2/meta/forms/{formViewId}` | Update Form View | +| `PATCH` | `/api/v2/meta/galleries/{galleryViewId}` | Update Gallery View | +| `PATCH` | `/api/v2/meta/grid-columns/{columnId}` | Update Grid View Column | +| `PATCH` | `/api/v2/meta/grids/{viewId}` | Update Grid View | +| `PATCH` | `/api/v2/meta/kanbans/{kanbanViewId}` | Update Kanban View | +| `PATCH` | `/api/v2/meta/maps/{mapViewId}` | Update Map View | +| `PATCH` | `/api/v2/meta/sorts/{sortId}` | Update Sort | +| `PATCH` | `/api/v2/meta/user/profile` | Update User Profile | +| `PATCH` | `/api/v2/meta/workspaces/{workspaceId}` | Update workspace ☁ | +| `PATCH` | `/api/v2/meta/workspaces/{workspaceId}/users/{userId}` | Update workspace user ☁ | +| `POST` | `/api/v2/meta/axiosRequestMake` | Axios Request | +| `POST` | `/api/v2/meta/bases/` | Create Base (OSS) | +| `POST` | `/api/v2/meta/bases/{baseId}/api-tokens` | Create API Token | +| `POST` | `/api/v2/meta/bases/{baseId}/meta-diff` | Sync Meta | +| `POST` | `/api/v2/meta/bases/{baseId}/meta-diff/{sourceId}` | Synchronise Source Meta | +| `POST` | `/api/v2/meta/bases/{baseId}/shared` | Create Base Shared Base | +| `POST` | `/api/v2/meta/bases/{baseId}/sources/` | Create Source | +| `POST` | `/api/v2/meta/bases/{baseId}/sources/{sourceId}/share/erd` | share ERD view | +| `POST` | `/api/v2/meta/bases/{baseId}/users` | Create Base User | +| `POST` | `/api/v2/meta/bases/{baseId}/users/{userId}/resend-invite` | Resend User Invitation | +| `POST` | `/api/v2/meta/bases/{baseId}/visibility-rules` | Create UI ACL | +| `POST` | `/api/v2/meta/comments` | Add Comment | +| `POST` | `/api/v2/meta/connection/test` | Test DB Connection | +| `POST` | `/api/v2/meta/duplicate/{baseId}` | Duplicate Base | +| `POST` | `/api/v2/meta/duplicate/{baseId}/{sourceId}` | Duplicate Base Source | +| `POST` | `/api/v2/meta/workspaces` | Create workspaces ☁ | +| `POST` | `/api/v2/meta/workspaces/{workspaceId}/bases` | Create Base | +| `POST` | `/api/v2/meta/workspaces/{workspaceId}/invitations` | Workspace user invite ☁ | + +#### Other + +| Method | Path | Summary | +|--------|------|---------| +| `POST` | `/api/v2/export/{viewId}/{exportAs}` | Trigger export as job | +| `POST` | `/api/v2/jobs/{baseId}` | Get Jobs | + +#### Table Operations + +| Method | Path | Summary | +|--------|------|---------| +| `DELETE` | `/api/v2/meta/tables/{tableId}` | Delete Table | +| `GET` | `/api/v2/meta/bases/{baseId}/tables` | List Tables | +| `GET` | `/api/v2/meta/bases/{baseId}/{sourceId}/tables` | List Tables | +| `GET` | `/api/v2/meta/tables/{tableId}` | Get Table Metadata | +| `GET` | `/api/v2/meta/tables/{tableId}/columns/hash` | Get columns hash for table | +| `GET` | `/api/v2/meta/tables/{tableId}/hooks` | List Table Hooks | +| `GET` | `/api/v2/meta/tables/{tableId}/hooks/samplePayload/{operation}/{version}` | Get Sample Hook Payload | +| `GET` | `/api/v2/meta/tables/{tableId}/share` | List Shared Views | +| `GET` | `/api/v2/meta/tables/{tableId}/views` | List Views | +| `PATCH` | `/api/v2/meta/tables/{tableId}` | Update Table | +| `POST` | `/api/v2/meta/bases/{baseId}/tables` | Create Table | +| `POST` | `/api/v2/meta/bases/{baseId}/{sourceId}/tables` | Create Table | +| `POST` | `/api/v2/meta/duplicate/{baseId}/table/{tableId}` | Duplicate Table | +| `POST` | `/api/v2/meta/tables/{tableId}/columns` | Create Column | +| `POST` | `/api/v2/meta/tables/{tableId}/columns/bulk` | Bulk create-update-delete columns | +| `POST` | `/api/v2/meta/tables/{tableId}/forms` | Create Form View | +| `POST` | `/api/v2/meta/tables/{tableId}/galleries` | Create Gallery View | +| `POST` | `/api/v2/meta/tables/{tableId}/grids` | Create Grid View | +| `POST` | `/api/v2/meta/tables/{tableId}/hooks` | Create Table Hook | +| `POST` | `/api/v2/meta/tables/{tableId}/hooks/test` | Test Hook | +| `POST` | `/api/v2/meta/tables/{tableId}/kanbans` | Create Kanban View | +| `POST` | `/api/v2/meta/tables/{tableId}/maps` | Create Map View | +| `POST` | `/api/v2/meta/tables/{tableId}/reorder` | Reorder Table | + +#### View Operations + +| Method | Path | Summary | +|--------|------|---------| +| `DELETE` | `/api/v2/meta/views/{viewId}` | Delete View | +| `DELETE` | `/api/v2/meta/views/{viewId}/share` | Delete Shared View | +| `GET` | `/api/v2/meta/views/{viewId}/filters` | List View Filters | +| `GET` | `/api/v2/meta/views/{viewId}/sorts` | List View Sorts | +| `PATCH` | `/api/v2/meta/views/{viewId}` | Update View | +| `PATCH` | `/api/v2/meta/views/{viewId}/share` | Update Shared View | +| `POST` | `/api/v2/meta/views/{viewId}/filters` | Create View Filter | +| `POST` | `/api/v2/meta/views/{viewId}/hide-all` | Hide All Columns In View | +| `POST` | `/api/v2/meta/views/{viewId}/share` | Create Shared View | +| `POST` | `/api/v2/meta/views/{viewId}/show-all` | Show All Columns In View | +| `POST` | `/api/v2/meta/views/{viewId}/sorts` | Create View Sort | + +#### Webhook Operations + +| Method | Path | Summary | +|--------|------|---------| +| `DELETE` | `/api/v2/meta/hooks/{hookId}` | Delete Table Hook | +| `GET` | `/api/v2/meta/hooks/{hookId}/filters` | Get Table Hook Filter | +| `GET` | `/api/v2/meta/hooks/{hookId}/logs` | List Hook Logs | +| `PATCH` | `/api/v2/meta/hooks/{hookId}` | Update Table Hook | +| `POST` | `/api/v2/meta/hooks/{hookId}/filters` | Create Table Hook Filter | + +### New Endpoints + +#### Record Operations + +| Method | Path | Summary | +|--------|------|---------| +| `DELETE` | `/api/v3/data/{baseId}/{tableId}/links/{linkFieldId}/{recordId}` | Unlink Records | +| `DELETE` | `/api/v3/data/{baseId}/{tableId}/records` | Delete Table Records | +| `GET` | `/api/v3/data/{baseId}/{tableId}/count` | Count Table Records | +| `GET` | `/api/v3/data/{baseId}/{tableId}/links/{linkFieldId}/{recordId}` | List Linked Records | +| `GET` | `/api/v3/data/{baseId}/{tableId}/records` | List Table Records | +| `GET` | `/api/v3/data/{baseId}/{tableId}/records/{recordId}` | Read Table Record | +| `PATCH` | `/api/v3/data/{baseId}/{tableId}/records` | Update Table Records | +| `POST` | `/api/v3/data/{baseId}/{modelId}/records/{recordId}/fields/{fieldId}/upload` | Upload Attachment to Cell | +| `POST` | `/api/v3/data/{baseId}/{tableId}/links/{linkFieldId}/{recordId}` | Link Records | +| `POST` | `/api/v3/data/{baseId}/{tableId}/records` | Create Table Records | + +--- + +## Data API: v2 → v3 Differences + +### Removed Endpoints + +#### File Operations + +| Method | Path | Summary | +|--------|------|---------| +| `POST` | `/api/v2/storage/upload` | Attachment Upload | + +#### Table Operations + +| Method | Path | Summary | +|--------|------|---------| +| `DELETE` | `/api/v2/tables/{tableId}/links/{linkFieldId}/records/{recordId}` | Unlink Records | +| `DELETE` | `/api/v2/tables/{tableId}/records` | Delete Table Records | +| `GET` | `/api/v2/tables/{tableId}/links/{linkFieldId}/records/{recordId}` | List Linked Records | +| `GET` | `/api/v2/tables/{tableId}/records` | List Table Records | +| `GET` | `/api/v2/tables/{tableId}/records/count` | Count Table Records | +| `GET` | `/api/v2/tables/{tableId}/records/{recordId}` | Read Table Record | +| `PATCH` | `/api/v2/tables/{tableId}/records` | Update Table Records | +| `POST` | `/api/v2/tables/{tableId}/links/{linkFieldId}/records/{recordId}` | Link Records | +| `POST` | `/api/v2/tables/{tableId}/records` | Create Table Records | + +### New Endpoints + +#### Column/Field Operations + +| Method | Path | Summary | +|--------|------|---------| +| `DELETE` | `/api/v3/meta/bases/{baseId}/fields/{fieldId}` | Delete field | +| `GET` | `/api/v3/meta/bases/{baseId}/fields/{fieldId}` | Get field | +| `PATCH` | `/api/v3/meta/bases/{baseId}/fields/{fieldId}` | Update field | + +#### Meta Operations + +| Method | Path | Summary | +|--------|------|---------| +| `DELETE` | `/api/v3/meta/bases/{baseId}` | Delete base | +| `DELETE` | `/api/v3/meta/bases/{base_id}/members` | Delete base members | +| `DELETE` | `/api/v3/meta/workspaces/{workspaceId}/members` | Delete workspace members | +| `GET` | `/api/v3/meta/bases/{baseId}` | Get base meta | +| `GET` | `/api/v3/meta/bases/{baseId}?include[]=members` | List base members | +| `GET` | `/api/v3/meta/workspaces/{workspaceId}/bases` | List bases | +| `GET` | `/api/v3/meta/workspaces/{workspaceId}?include[]=members` | List workspace members | +| `PATCH` | `/api/v3/meta/bases/{baseId}` | Update base | +| `PATCH` | `/api/v3/meta/bases/{base_id}/members` | Update base members | +| `PATCH` | `/api/v3/meta/workspaces/{workspaceId}/members` | Update workspace members | +| `POST` | `/api/v3/meta/bases/{base_id}/members` | Invite base members | +| `POST` | `/api/v3/meta/workspaces/{workspaceId}/bases` | Create base | +| `POST` | `/api/v3/meta/workspaces/{workspaceId}/members` | Add workspace members | + +#### Table Operations + +| Method | Path | Summary | +|--------|------|---------| +| `DELETE` | `/api/v3/meta/bases/{baseId}/tables/{tableId}` | Delete table | +| `GET` | `/api/v3/meta/bases/{baseId}/tables/{tableId}` | Get table schema | +| `GET` | `/api/v3/meta/bases/{baseId}/tables/{tableId}/views` | List views | +| `GET` | `/api/v3/meta/bases/{base_id}/tables` | List tables | +| `PATCH` | `/api/v3/meta/bases/{baseId}/tables/{tableId}` | Update table | +| `POST` | `/api/v3/meta/bases/{baseId}/tables/{tableId}/fields` | Create field | +| `POST` | `/api/v3/meta/bases/{baseId}/tables/{tableId}/views` | Create view | +| `POST` | `/api/v3/meta/bases/{base_id}/tables` | Create table | + +#### View Operations + +| Method | Path | Summary | +|--------|------|---------| +| `DELETE` | `/api/v3/meta/bases/{baseId}/views/{viewId}` | Delete view | +| `DELETE` | `/api/v3/meta/bases/{baseId}/views/{viewId}/filters` | Delete filter | +| `DELETE` | `/api/v3/meta/bases/{baseId}/views/{viewId}/sorts` | Delete sort | +| `GET` | `/api/v3/meta/bases/{baseId}/views/{viewId}` | Get view schema | +| `GET` | `/api/v3/meta/bases/{baseId}/views/{viewId}/filters` | List view filters | +| `GET` | `/api/v3/meta/bases/{baseId}/views/{viewId}/sorts` | List view sorts | +| `PATCH` | `/api/v3/meta/bases/{baseId}/views/{viewId}` | Update view | +| `PATCH` | `/api/v3/meta/bases/{baseId}/views/{viewId}/filters` | Update filter | +| `PATCH` | `/api/v3/meta/bases/{baseId}/views/{viewId}/sorts` | Update sort | +| `POST` | `/api/v3/meta/bases/{baseId}/views/{viewId}/filters` | Create filter | +| `POST` | `/api/v3/meta/bases/{baseId}/views/{viewId}/sorts` | Add sort | +| `PUT` | `/api/v3/meta/bases/{baseId}/views/{viewId}/filters` | Replace filter | + +--- + +## Breaking Changes Summary + +These changes will require code modifications: + +### Meta API Breaking Changes +#### Authentication +- **Removed:** `GET /api/v2/auth/user/me` +- **Removed:** `POST /api/v2/auth/email/validate/{token}` +- **Removed:** `POST /api/v2/auth/password/change` +- **Removed:** `POST /api/v2/auth/password/forgot` +- **Removed:** `POST /api/v2/auth/password/reset/{token}` +- **Removed:** `POST /api/v2/auth/token/refresh` +- **Removed:** `POST /api/v2/auth/token/validate/{token}` +- **Removed:** `POST /api/v2/auth/user/signin` +- **Removed:** `POST /api/v2/auth/user/signout` +- **Removed:** `POST /api/v2/auth/user/signup` + +#### Column/Field Operations +- **Removed:** `DELETE /api/v2/meta/columns/{columnId}` +- **Removed:** `GET /api/v2/meta/columns/{columnId}` +- **Removed:** `GET /api/v2/meta/views/{viewId}/columns` +- **Removed:** `PATCH /api/v2/meta/columns/{columnId}` +- **Removed:** `PATCH /api/v2/meta/views/{viewId}/columns/{columnId}` +- **Removed:** `POST /api/v2/meta/columns/{columnId}/primary` +- **Removed:** `POST /api/v2/meta/views/{viewId}/columns` + +#### File Operations +- **Removed:** `POST /api/v2/storage/upload` + +#### Meta Operations +- **Removed:** `DELETE /api/v2/meta/bases/{baseId}` +- **Removed:** `DELETE /api/v2/meta/bases/{baseId}/api-tokens/{tokenId}` +- **Removed:** `DELETE /api/v2/meta/bases/{baseId}/shared` +- **Removed:** `DELETE /api/v2/meta/bases/{baseId}/sources/{sourceId}` +- **Removed:** `DELETE /api/v2/meta/bases/{baseId}/sources/{sourceId}/share/erd` +- **Removed:** `DELETE /api/v2/meta/bases/{baseId}/users/{userId}` +- **Removed:** `DELETE /api/v2/meta/cache` +- **Removed:** `DELETE /api/v2/meta/comment/{commentId}` +- **Removed:** `DELETE /api/v2/meta/filters/{filterId}` +- **Removed:** `DELETE /api/v2/meta/sorts/{sortId}` +- **Removed:** `DELETE /api/v2/meta/workspaces/{workspaceId}` +- **Removed:** `DELETE /api/v2/meta/workspaces/{workspaceId}/users/{userId}` +- **Removed:** `GET /api/v2/meta/bases/` +- **Removed:** `GET /api/v2/meta/bases/{baseId}` +- **Removed:** `GET /api/v2/meta/bases/{baseId}/api-tokens` +- **Removed:** `GET /api/v2/meta/bases/{baseId}/cost` +- **Removed:** `GET /api/v2/meta/bases/{baseId}/has-empty-or-null-filters` +- **Removed:** `GET /api/v2/meta/bases/{baseId}/info` +- **Removed:** `GET /api/v2/meta/bases/{baseId}/meta-diff` +- **Removed:** `GET /api/v2/meta/bases/{baseId}/meta-diff/{sourceId}` +- **Removed:** `GET /api/v2/meta/bases/{baseId}/shared` +- **Removed:** `GET /api/v2/meta/bases/{baseId}/sources/` +- **Removed:** `GET /api/v2/meta/bases/{baseId}/sources/{sourceId}` +- **Removed:** `GET /api/v2/meta/bases/{baseId}/users` +- **Removed:** `GET /api/v2/meta/bases/{baseId}/visibility-rules` +- **Removed:** `GET /api/v2/meta/cache` +- **Removed:** `GET /api/v2/meta/comments` +- **Removed:** `GET /api/v2/meta/comments/count` +- **Removed:** `GET /api/v2/meta/filters/{filterGroupId}/children` +- **Removed:** `GET /api/v2/meta/filters/{filterId}` +- **Removed:** `GET /api/v2/meta/forms/{formViewId}` +- **Removed:** `GET /api/v2/meta/galleries/{galleryViewId}` +- **Removed:** `GET /api/v2/meta/grids/{gridId}/grid-columns` +- **Removed:** `GET /api/v2/meta/kanbans/{kanbanViewId}` +- **Removed:** `GET /api/v2/meta/maps/{mapViewId}` +- **Removed:** `GET /api/v2/meta/nocodb/info` +- **Removed:** `GET /api/v2/meta/sorts/{sortId}` +- **Removed:** `GET /api/v2/meta/workspaces` +- **Removed:** `GET /api/v2/meta/workspaces/{workspaceId}` +- **Removed:** `GET /api/v2/meta/workspaces/{workspaceId}/bases` +- **Removed:** `GET /api/v2/meta/workspaces/{workspaceId}/users` +- **Removed:** `GET /api/v2/meta/workspaces/{workspaceId}/users/{userId}` +- **Removed:** `PATCH /api/v2/meta/bases/{baseId}` +- **Removed:** `PATCH /api/v2/meta/bases/{baseId}/shared` +- **Removed:** `PATCH /api/v2/meta/bases/{baseId}/sources/{sourceId}` +- **Removed:** `PATCH /api/v2/meta/bases/{baseId}/user` +- **Removed:** `PATCH /api/v2/meta/bases/{baseId}/users/{userId}` +- **Removed:** `PATCH /api/v2/meta/comment/{commentId}` +- **Removed:** `PATCH /api/v2/meta/filters/{filterId}` +- **Removed:** `PATCH /api/v2/meta/form-columns/{formViewColumnId}` +- **Removed:** `PATCH /api/v2/meta/forms/{formViewId}` +- **Removed:** `PATCH /api/v2/meta/galleries/{galleryViewId}` +- **Removed:** `PATCH /api/v2/meta/grid-columns/{columnId}` +- **Removed:** `PATCH /api/v2/meta/grids/{viewId}` +- **Removed:** `PATCH /api/v2/meta/kanbans/{kanbanViewId}` +- **Removed:** `PATCH /api/v2/meta/maps/{mapViewId}` +- **Removed:** `PATCH /api/v2/meta/sorts/{sortId}` +- **Removed:** `PATCH /api/v2/meta/user/profile` +- **Removed:** `PATCH /api/v2/meta/workspaces/{workspaceId}` +- **Removed:** `PATCH /api/v2/meta/workspaces/{workspaceId}/users/{userId}` +- **Removed:** `POST /api/v2/meta/axiosRequestMake` +- **Removed:** `POST /api/v2/meta/bases/` +- **Removed:** `POST /api/v2/meta/bases/{baseId}/api-tokens` +- **Removed:** `POST /api/v2/meta/bases/{baseId}/meta-diff` +- **Removed:** `POST /api/v2/meta/bases/{baseId}/meta-diff/{sourceId}` +- **Removed:** `POST /api/v2/meta/bases/{baseId}/shared` +- **Removed:** `POST /api/v2/meta/bases/{baseId}/sources/` +- **Removed:** `POST /api/v2/meta/bases/{baseId}/sources/{sourceId}/share/erd` +- **Removed:** `POST /api/v2/meta/bases/{baseId}/users` +- **Removed:** `POST /api/v2/meta/bases/{baseId}/users/{userId}/resend-invite` +- **Removed:** `POST /api/v2/meta/bases/{baseId}/visibility-rules` +- **Removed:** `POST /api/v2/meta/comments` +- **Removed:** `POST /api/v2/meta/connection/test` +- **Removed:** `POST /api/v2/meta/duplicate/{baseId}` +- **Removed:** `POST /api/v2/meta/duplicate/{baseId}/{sourceId}` +- **Removed:** `POST /api/v2/meta/workspaces` +- **Removed:** `POST /api/v2/meta/workspaces/{workspaceId}/bases` +- **Removed:** `POST /api/v2/meta/workspaces/{workspaceId}/invitations` + +#### Other +- **Removed:** `POST /api/v2/export/{viewId}/{exportAs}` +- **Removed:** `POST /api/v2/jobs/{baseId}` + +#### Table Operations +- **Removed:** `DELETE /api/v2/meta/tables/{tableId}` +- **Removed:** `GET /api/v2/meta/bases/{baseId}/tables` +- **Removed:** `GET /api/v2/meta/bases/{baseId}/{sourceId}/tables` +- **Removed:** `GET /api/v2/meta/tables/{tableId}` +- **Removed:** `GET /api/v2/meta/tables/{tableId}/columns/hash` +- **Removed:** `GET /api/v2/meta/tables/{tableId}/hooks` +- **Removed:** `GET /api/v2/meta/tables/{tableId}/hooks/samplePayload/{operation}/{version}` +- **Removed:** `GET /api/v2/meta/tables/{tableId}/share` +- **Removed:** `GET /api/v2/meta/tables/{tableId}/views` +- **Removed:** `PATCH /api/v2/meta/tables/{tableId}` +- **Removed:** `POST /api/v2/meta/bases/{baseId}/tables` +- **Removed:** `POST /api/v2/meta/bases/{baseId}/{sourceId}/tables` +- **Removed:** `POST /api/v2/meta/duplicate/{baseId}/table/{tableId}` +- **Removed:** `POST /api/v2/meta/tables/{tableId}/columns` +- **Removed:** `POST /api/v2/meta/tables/{tableId}/columns/bulk` +- **Removed:** `POST /api/v2/meta/tables/{tableId}/forms` +- **Removed:** `POST /api/v2/meta/tables/{tableId}/galleries` +- **Removed:** `POST /api/v2/meta/tables/{tableId}/grids` +- **Removed:** `POST /api/v2/meta/tables/{tableId}/hooks` +- **Removed:** `POST /api/v2/meta/tables/{tableId}/hooks/test` +- **Removed:** `POST /api/v2/meta/tables/{tableId}/kanbans` +- **Removed:** `POST /api/v2/meta/tables/{tableId}/maps` +- **Removed:** `POST /api/v2/meta/tables/{tableId}/reorder` + +#### View Operations +- **Removed:** `DELETE /api/v2/meta/views/{viewId}` +- **Removed:** `DELETE /api/v2/meta/views/{viewId}/share` +- **Removed:** `GET /api/v2/meta/views/{viewId}/filters` +- **Removed:** `GET /api/v2/meta/views/{viewId}/sorts` +- **Removed:** `PATCH /api/v2/meta/views/{viewId}` +- **Removed:** `PATCH /api/v2/meta/views/{viewId}/share` +- **Removed:** `POST /api/v2/meta/views/{viewId}/filters` +- **Removed:** `POST /api/v2/meta/views/{viewId}/hide-all` +- **Removed:** `POST /api/v2/meta/views/{viewId}/share` +- **Removed:** `POST /api/v2/meta/views/{viewId}/show-all` +- **Removed:** `POST /api/v2/meta/views/{viewId}/sorts` + +#### Webhook Operations +- **Removed:** `DELETE /api/v2/meta/hooks/{hookId}` +- **Removed:** `GET /api/v2/meta/hooks/{hookId}/filters` +- **Removed:** `GET /api/v2/meta/hooks/{hookId}/logs` +- **Removed:** `PATCH /api/v2/meta/hooks/{hookId}` +- **Removed:** `POST /api/v2/meta/hooks/{hookId}/filters` + +### Data API Breaking Changes +#### File Operations +- **Removed:** `POST /api/v2/storage/upload` + +#### Table Operations +- **Removed:** `DELETE /api/v2/tables/{tableId}/links/{linkFieldId}/records/{recordId}` +- **Removed:** `DELETE /api/v2/tables/{tableId}/records` +- **Removed:** `GET /api/v2/tables/{tableId}/links/{linkFieldId}/records/{recordId}` +- **Removed:** `GET /api/v2/tables/{tableId}/records` +- **Removed:** `GET /api/v2/tables/{tableId}/records/count` +- **Removed:** `GET /api/v2/tables/{tableId}/records/{recordId}` +- **Removed:** `PATCH /api/v2/tables/{tableId}/records` +- **Removed:** `POST /api/v2/tables/{tableId}/links/{linkFieldId}/records/{recordId}` +- **Removed:** `POST /api/v2/tables/{tableId}/records` + +--- + +## Implementation Recommendations + +### Version Detection Strategy +```typescript +// Detect API version from server response +async function detectApiVersion(baseUrl: string): Promise<'v2' | 'v3'> { + // Check for v3-specific endpoints or response structures + // Implementation depends on specific differences found +} +``` + +### Adapter Pattern +```typescript +interface ApiAdapter { + getTables(baseId: string): Promise; + getRecords(tableId: string, params?: QueryParams): Promise; + // ... other methods +} + +class ApiV2Adapter implements ApiAdapter { /* ... */ } +class ApiV3Adapter implements ApiAdapter { /* ... */ } +``` + +### Migration Priority +1. **High Priority**: Endpoints used frequently (record CRUD, table listing) +2. **Medium Priority**: View operations, link management +3. **Low Priority**: Advanced features, admin operations diff --git a/docs/API_V2_V3_MIGRATION_GUIDE.md b/docs/API_V2_V3_MIGRATION_GUIDE.md new file mode 100644 index 0000000..1880026 --- /dev/null +++ b/docs/API_V2_V3_MIGRATION_GUIDE.md @@ -0,0 +1,644 @@ +# NocoDB API v2 to v3 Migration Guide + +**Executive Summary for Developers** + +--- + +## Critical Discovery: API File Role Reversal + +**The most important finding is that v2 and v3 have INVERTED their API file definitions:** + +| Version | "Meta API" File | "Data API" File | +|---------|----------------|----------------| +| **v2** | Schema/Structure operations (137 endpoints) | Record CRUD operations (10 endpoints) | +| **v3** | Record CRUD operations (10 endpoints) | Schema/Structure operations (36 endpoints) | + +**This means:** +- What was called "Meta API" in v2 is now called "Data API" in v3 +- What was called "Data API" in v2 is now called "Meta API" in v3 + +**This is NOT just a naming change - you must load the opposite file for equivalent operations!** + +--- + +## Breaking Changes by Priority + +### 🔴 CRITICAL - Every endpoint requires baseId + +**Impact:** 100% of code must change + +All v3 paths now require `baseId` as a path parameter: + +```diff +- GET /api/v2/tables/{tableId}/records ++ GET /api/v3/data/{baseId}/{tableId}/records + +- GET /api/v2/meta/tables/{tableId} ++ GET /api/v3/meta/bases/{baseId}/tables/{tableId} +``` + +**Challenge:** v2 code often doesn't track baseId for each tableId. + +**Solution:** Implement a baseId resolver or require baseId in all method signatures. + +--- + +### 🔴 CRITICAL - Pagination completely changed + +| Aspect | v2 | v3 | Breaking? | +|--------|----|----|-----------| +| **Offset** | `offset=25` | Removed - use `page` | ✅ YES | +| **Limit** | `limit=100` | `pageSize=100` | ✅ YES | +| **Page** | Not available | `page=2` | ✅ YES | +| **Nested Pagination** | Not available | `nestedPage=2` | ⚠️ NEW | + +**Migration:** +```typescript +// v2 +const params = { offset: 50, limit: 25 }; + +// v3 equivalent +const params = { page: 3, pageSize: 25 }; // page 3 = skip 50 records +``` + +--- + +### 🟡 MEDIUM - Query Parameter Changes + +#### Sort Format Changed + +**v2 - String format:** +``` +sort=field1,-field2 +``` + +**v3 - JSON format:** +``` +sort=[{"direction":"asc","field":"field1"},{"direction":"desc","field":"field2"}] +``` + +#### Fields Format Enhanced + +**v2:** +``` +fields=field1,field2 +``` + +**v3 - Array or string:** +``` +fields=["field1","field2"] OR fields=field1,field2 +``` + +#### Where Comparison Operators + +| Operator | v2 | v3 | Changed? | +|----------|----|----|----------| +| Not equal | `ne` | `neq` | ✅ YES | +| Others | Same | Same | ❌ NO | + +--- + +### 🔵 LOW - Terminology Changes + +| v2 Term | v3 Term | +|---------|---------| +| column | field | +| columnId | fieldId | + +--- + +## Most Used Endpoints Migration + +### 1. List Records + +```typescript +// v2 +GET /api/v2/tables/{tableId}/records + ?fields=field1,field2 + &sort=field1,-field2 + &where=(field1,eq,value) + &offset=0 + &limit=100 + &viewId={viewId} + +// v3 +GET /api/v3/data/{baseId}/{tableId}/records + ?fields=["field1","field2"] + &sort=[{"direction":"asc","field":"field1"},{"direction":"desc","field":"field2"}] + &where=(field1,eq,value) + &page=1 + &pageSize=100 + &viewId={viewId} +``` + +**Required Changes:** +1. Add `baseId` to path +2. Change `offset/limit` to `page/pageSize` +3. Update `sort` format to JSON +4. Update `fields` format (optional - string still works) + +--- + +### 2. Get Record + +```typescript +// v2 +GET /api/v2/tables/{tableId}/records/{recordId} + +// v3 +GET /api/v3/data/{baseId}/{tableId}/records/{recordId} +``` + +**Required Changes:** +1. Add `baseId` to path + +--- + +### 3. Create Records + +```typescript +// v2 +POST /api/v2/tables/{tableId}/records +Body: { field1: value1, field2: value2 } + +// v3 +POST /api/v3/data/{baseId}/{tableId}/records +Body: (same structure - verify with testing) +``` + +**Required Changes:** +1. Add `baseId` to path +2. Verify request body structure hasn't changed + +--- + +### 4. Update Records + +```typescript +// v2 +PATCH /api/v2/tables/{tableId}/records +Body: [{ Id: "rec123", field1: newValue }] + +// v3 +PATCH /api/v3/data/{baseId}/{tableId}/records +Body: (same structure - verify with testing) +``` + +**Required Changes:** +1. Add `baseId` to path +2. Verify request body structure (especially `Id` vs `id`) + +--- + +### 5. Delete Records + +```typescript +// v2 +DELETE /api/v2/tables/{tableId}/records +Body: [{ Id: "rec123" }] + +// v3 +DELETE /api/v3/data/{baseId}/{tableId}/records +Body: (same structure - verify with testing) +``` + +**Required Changes:** +1. Add `baseId` to path + +--- + +### 6. Count Records + +```typescript +// v2 +GET /api/v2/tables/{tableId}/records/count + +// v3 +GET /api/v3/data/{baseId}/{tableId}/count +``` + +**Required Changes:** +1. Add `baseId` to path +2. Path structure changed (`/count` not `/records/count`) + +--- + +### 7. List Tables + +```typescript +// v2 +GET /api/v2/meta/bases/{baseId}/tables + +// v3 +GET /api/v3/meta/bases/{baseId}/tables +``` + +**Required Changes:** +1. Load from different OpenAPI file (now in "Data API" spec) +2. Path is the same + +--- + +### 8. Get Table + +```typescript +// v2 +GET /api/v2/meta/tables/{tableId} + +// v3 +GET /api/v3/meta/bases/{baseId}/tables/{tableId} +``` + +**Required Changes:** +1. Add `baseId` to path +2. Load from different OpenAPI file + +--- + +### 9. Link Records + +```typescript +// v2 +POST /api/v2/tables/{tableId}/links/{linkFieldId}/records/{recordId} +Body: { linkedRecordIds: ["rec1", "rec2"] } + +// v3 +POST /api/v3/data/{baseId}/{tableId}/links/{linkFieldId}/{recordId} +Body: (verify structure) +``` + +**Required Changes:** +1. Add `baseId` to path +2. Path structure: removed `/records/` segment + +--- + +### 10. Unlink Records + +```typescript +// v2 +DELETE /api/v2/tables/{tableId}/links/{linkFieldId}/records/{recordId} +Body: { linkedRecordIds: ["rec1"] } + +// v3 +DELETE /api/v3/data/{baseId}/{tableId}/links/{linkFieldId}/{recordId} +Body: (verify structure) +``` + +**Required Changes:** +1. Add `baseId` to path +2. Path structure: removed `/records/` segment + +--- + +## Implementation Strategy + +### Option 1: Adapter Pattern (Recommended) + +Create a unified interface with version-specific implementations: + +```typescript +interface NocoDBRecordOperations { + list(tableId: string, params?: QueryParams): Promise; + get(tableId: string, recordId: string): Promise; + create(tableId: string, data: RecordData[]): Promise; + update(tableId: string, updates: RecordUpdate[]): Promise; + delete(tableId: string, recordIds: string[]): Promise; +} + +class V2RecordOperations implements NocoDBRecordOperations { + async list(tableId: string, params?: QueryParams) { + // Build v2 URL with offset/limit + const url = `${this.baseUrl}/api/v2/tables/${tableId}/records`; + // Convert params to v2 format + const v2Params = this.convertToV2Params(params); + return this.fetch(url, v2Params); + } +} + +class V3RecordOperations implements NocoDBRecordOperations { + constructor(private baseIdResolver: BaseIdResolver) {} + + async list(tableId: string, params?: QueryParams) { + // Resolve baseId + const baseId = await this.baseIdResolver.resolve(tableId); + // Build v3 URL + const url = `${this.baseUrl}/api/v3/data/${baseId}/${tableId}/records`; + // Convert params to v3 format + const v3Params = this.convertToV3Params(params); + return this.fetch(url, v3Params); + } + + private convertToV3Params(params?: QueryParams) { + if (!params) return {}; + + return { + fields: params.fields, // Already compatible + sort: this.convertSortToV3(params.sort), + where: params.where, // Mostly compatible (check 'ne' → 'neq') + page: params.page || Math.floor((params.offset || 0) / (params.limit || 25)) + 1, + pageSize: params.pageSize || params.limit || 25, + viewId: params.viewId + }; + } + + private convertSortToV3(sort?: string | object[]): object[] | undefined { + if (!sort) return undefined; + if (Array.isArray(sort)) return sort; // Already v3 format + + // Convert v2 string format to v3 object format + // "field1,-field2" → [{"direction":"asc","field":"field1"},{"direction":"desc","field":"field2"}] + return sort.split(',').map(field => { + const desc = field.startsWith('-'); + return { + direction: desc ? 'desc' : 'asc', + field: desc ? field.slice(1) : field + }; + }); + } +} +``` + +### Option 2: Query Parameter Adapter + +Create middleware to convert query parameters: + +```typescript +class QueryParamAdapter { + convertToV3(v2Params: V2QueryParams): V3QueryParams { + const { offset, limit, sort, where, ...rest } = v2Params; + + return { + ...rest, + page: offset !== undefined ? Math.floor(offset / (limit || 25)) + 1 : undefined, + pageSize: limit, + sort: typeof sort === 'string' ? this.convertSort(sort) : sort, + where: where?.replace(/,ne,/g, ',neq,') // Fix operator + }; + } + + convertSort(sortString: string): SortObject[] { + return sortString.split(',').map(field => { + const desc = field.startsWith('-'); + return { + direction: desc ? 'desc' : 'asc', + field: desc ? field.slice(1) : field + }; + }); + } +} +``` + +### Option 3: Base ID Resolution + +Implement caching strategy for baseId lookup: + +```typescript +class BaseIdResolver { + private cache = new Map(); // tableId → baseId + private cacheTime = new Map(); + private TTL = 3600000; // 1 hour + + async resolve(tableId: string): Promise { + // Check cache + const cached = this.cache.get(tableId); + const cacheAge = Date.now() - (this.cacheTime.get(tableId) || 0); + + if (cached && cacheAge < this.TTL) { + return cached; + } + + // Fetch from API + // Option A: If you have workspace/base context + if (this.currentBaseId) { + this.cache.set(tableId, this.currentBaseId); + this.cacheTime.set(tableId, Date.now()); + return this.currentBaseId; + } + + // Option B: Fetch table metadata (if available in v3) + // This depends on whether there's a v3 endpoint that returns baseId for a tableId + + // Option C: Fetch all bases and build complete mapping + await this.buildCompleteMapping(); + + const baseId = this.cache.get(tableId); + if (!baseId) { + throw new Error(`Cannot resolve baseId for table ${tableId}`); + } + + return baseId; + } + + async buildCompleteMapping(): Promise { + const workspaces = await this.api.listWorkspaces(); + for (const workspace of workspaces) { + const bases = await this.api.listBases(workspace.id); + for (const base of bases) { + const tables = await this.api.listTables(base.id); + for (const table of tables) { + this.cache.set(table.id, base.id); + this.cacheTime.set(table.id, Date.now()); + } + } + } + } + + // Call this whenever you fetch table metadata + cacheMapping(tableId: string, baseId: string): void { + this.cache.set(tableId, baseId); + this.cacheTime.set(tableId, Date.now()); + } +} +``` + +--- + +## Testing Checklist + +### Record Operations +- [ ] List records with all query parameters +- [ ] List records with pagination (verify page calculation) +- [ ] List records with sorting (verify JSON format) +- [ ] List records with filtering (verify 'neq' operator) +- [ ] Get single record +- [ ] Create single record +- [ ] Create multiple records +- [ ] Update single record +- [ ] Update multiple records +- [ ] Delete single record +- [ ] Delete multiple records +- [ ] Count records + +### Table Operations +- [ ] List tables in base +- [ ] Get table schema +- [ ] Create table +- [ ] Update table +- [ ] Delete table + +### Link Operations +- [ ] Link records +- [ ] Unlink records +- [ ] List linked records +- [ ] Verify path structure (no `/records/` segment) + +### View Operations +- [ ] List views +- [ ] Get view +- [ ] List records with viewId +- [ ] Verify view filtering works +- [ ] Verify view sorting works + +### Field Operations +- [ ] Get field (verify 'field' terminology) +- [ ] Create field +- [ ] Update field +- [ ] Delete field + +### Error Handling +- [ ] Verify error response structure +- [ ] Test rate limiting +- [ ] Test authentication errors +- [ ] Test not found errors + +### Pagination +- [ ] Verify page 1 returns first N records +- [ ] Verify page 2 returns next N records +- [ ] Verify pageSize works correctly +- [ ] Compare with v2 offset/limit results + +--- + +## Common Pitfalls + +### 1. Forgetting baseId +```typescript +// ❌ Will fail in v3 +const url = `/api/v3/data/${tableId}/records`; + +// ✅ Correct +const baseId = await resolver.resolve(tableId); +const url = `/api/v3/data/${baseId}/${tableId}/records`; +``` + +### 2. Using old pagination +```typescript +// ❌ v3 doesn't support offset/limit +const params = { offset: 25, limit: 100 }; + +// ✅ Use page/pageSize +const params = { page: 2, pageSize: 100 }; +``` + +### 3. Using wrong sort format +```typescript +// ❌ String format may not work consistently in v3 +const params = { sort: 'field1,-field2' }; + +// ✅ Use JSON format +const params = { + sort: [ + { direction: 'asc', field: 'field1' }, + { direction: 'desc', field: 'field2' } + ] +}; +``` + +### 4. Using 'ne' instead of 'neq' +```typescript +// ❌ v3 uses 'neq' for not equal +where=(field1,ne,value) + +// ✅ Correct operator +where=(field1,neq,value) +``` + +### 5. Loading from wrong OpenAPI file +```typescript +// ❌ v3 structure is inverted +// Don't assume "Meta API" has schema operations + +// ✅ Check the actual endpoints in each file +// v3: Meta API = records, Data API = schema +``` + +--- + +## Quick Reference + +### Path Changes Summary + +| Operation | v2 Path | v3 Path | +|-----------|---------|---------| +| List Records | `/api/v2/tables/{tableId}/records` | `/api/v3/data/{baseId}/{tableId}/records` | +| Get Record | `/api/v2/tables/{tableId}/records/{id}` | `/api/v3/data/{baseId}/{tableId}/records/{id}` | +| Create | `/api/v2/tables/{tableId}/records` | `/api/v3/data/{baseId}/{tableId}/records` | +| Update | `/api/v2/tables/{tableId}/records` | `/api/v3/data/{baseId}/{tableId}/records` | +| Delete | `/api/v2/tables/{tableId}/records` | `/api/v3/data/{baseId}/{tableId}/records` | +| Count | `/api/v2/tables/{tableId}/records/count` | `/api/v3/data/{baseId}/{tableId}/count` | +| Link | `/api/v2/tables/{tableId}/links/{fieldId}/records/{id}` | `/api/v3/data/{baseId}/{tableId}/links/{fieldId}/{id}` | +| List Tables | `/api/v2/meta/bases/{baseId}/tables` | `/api/v3/meta/bases/{baseId}/tables` | +| Get Table | `/api/v2/meta/tables/{tableId}` | `/api/v3/meta/bases/{baseId}/tables/{tableId}` | + +### Parameter Migration + +| v2 | v3 | Notes | +|----|----|----| +| `offset=N` | `page=M` | page = floor(offset/pageSize) + 1 | +| `limit=N` | `pageSize=N` | Direct replacement | +| `sort=f1,-f2` | `sort=[{...}]` | Convert to JSON array | +| `where=(...,ne,...)` | `where=(...,neq,...)` | Operator change | + +--- + +## Timeline Estimate + +| Phase | Duration | Tasks | +|-------|----------|-------| +| **Analysis** | 1 week | Review all endpoint usage, identify dependencies | +| **Architecture** | 1 week | Design adapter layer, base ID resolution | +| **Implementation** | 2-3 weeks | Implement v3 support, conversion utilities | +| **Testing** | 2 weeks | Integration tests, manual testing, edge cases | +| **Deployment** | 1 week | Feature flag, gradual rollout, monitoring | +| **Total** | **7-9 weeks** | For complete dual-version support | + +--- + +## Risk Assessment + +| Risk | Impact | Likelihood | Mitigation | +|------|--------|------------|------------| +| Base ID resolution fails | HIGH | MEDIUM | Cache baseId proactively, add fallbacks | +| Query parameter incompatibility | HIGH | MEDIUM | Comprehensive conversion layer, extensive testing | +| Response schema changes | MEDIUM | MEDIUM | Version-specific response parsers | +| Performance degradation | MEDIUM | LOW | Optimize base ID caching, benchmark both versions | +| Authentication incompatibility | HIGH | LOW | Verify token format early, test auth flows | + +--- + +## Resources + +- **Full Comparison Report:** `NOCODB_API_V2_V3_COMPARISON.md` +- **Schema Analysis:** `NOCODB_API_SCHEMA_COMPARISON.md` +- **OpenAPI Specs:** + - v2 Meta: `docs/nocodb-openapi-meta.json` + - v2 Data: `docs/nocodb-openapi-data.json` + - v3 Meta: `docs/nocodb-openapi-meta-v3.json` + - v3 Data: `docs/nocodb-openapi-data-v3.json` + +--- + +## Conclusion + +The v2 to v3 migration is a **major breaking change** requiring significant code modifications. The most critical changes are: + +1. **baseId now required in all paths** +2. **Pagination completely redesigned** (offset/limit → page/pageSize) +3. **API file definitions inverted** (Meta ↔ Data) +4. **Sort format changed** to JSON +5. **Path structures modified** for several operations + +**Recommendation:** Implement adapter pattern with automatic version detection to maintain backward compatibility while adding v3 support. + +**Estimated Effort:** 7-9 weeks for complete implementation and testing. diff --git a/docs/NOCODB_API_SCHEMA_COMPARISON.md b/docs/NOCODB_API_SCHEMA_COMPARISON.md new file mode 100644 index 0000000..4d1a568 --- /dev/null +++ b/docs/NOCODB_API_SCHEMA_COMPARISON.md @@ -0,0 +1,176 @@ +# NocoDB API v2 to v3 Schema & Parameter Comparison + +**Generated:** 2025-10-10 11:35:33 + +This report focuses on the detailed schema and parameter changes between v2 and v3. + +## Record Operations Detailed Analysis + +### List Records Query Parameters + +#### v2 Query Parameters + +| Parameter | Required | Type | Description | +|-----------|----------|------|-------------| +| `fields` | No | string | Allows you to specify the fields that you wish to include in your API response. By default, all the fields are included in the response. + +Example: `fields=field1,field2` will include only 'field1' and 'field2' in the API response. + +Please note that it's essential not to include spaces between field names in the comma-separated list. | +| `sort` | No | string | Allows you to specify the fields by which you want to sort the records in your API response. By default, sorting is done in ascending order for the designated fields. To sort in descending order, add a '-' symbol before the field name. + +Example: `sort=field1,-field2` will sort the records first by 'field1' in ascending order and then by 'field2' in descending order. + +If `viewId` query parameter is also included, the sort included here will take precedence over any sorting configuration defined in the view. + +Please note that it's essential not to include spaces between field names in the comma-separated list. | +| `where` | No | string | Enables you to define specific conditions for filtering records in your API response. Multiple conditions can be combined using logical operators such as 'and' and 'or'. Each condition consists of three parts: a field name, a comparison operator, and a value. + +Example: `where=(field1,eq,value1)~and(field2,eq,value2)` will filter records where 'field1' is equal to 'value1' AND 'field2' is equal to 'value2'. + +You can also use other comparison operators like 'ne' (not equal), 'gt' (greater than), 'lt' (less than), and more, to create complex filtering rules. + +If `viewId` query parameter is also included, then the filters included here will be applied over the filtering configuration defined in the view. + +Please remember to maintain the specified format, and do not include spaces between the different condition components | +| `offset` | No | integer | Enables you to control the pagination of your API response by specifying the number of records you want to skip from the beginning of the result set. The default value for this parameter is set to 0, meaning no records are skipped by default. + +Example: `offset=25` will skip the first 25 records in your API response, allowing you to access records starting from the 26th position. + +Please note that the 'offset' value represents the number of records to exclude, not an index value, so an offset of 25 will skip the first 25 records. | +| `limit` | No | integer | Enables you to set a limit on the number of records you want to retrieve in your API response. By default, your response includes all the available records, but by using this parameter, you can control the quantity you receive. + +Example: `limit=100` will constrain your response to the first 100 records in the dataset. | +| `viewId` | No | string | ***View Identifier***. Allows you to fetch records that are currently visible within a specific view. API retrieves records in the order they are displayed if the SORT option is enabled within that view. + +Additionally, if you specify a `sort` query parameter, it will take precedence over any sorting configuration defined in the view. If you specify a `where` query parameter, it will be applied over the filtering configuration defined in the view. + +By default, all fields, including those that are disabled within the view, are included in the response. To explicitly specify which fields to include or exclude, you can use the `fields` query parameter to customize the output according to your requirements. | + +#### v3 Query Parameters + +| Parameter | Required | Type | Description | +|-----------|----------|------|-------------| +| `fields` | No | None | Allows you to specify the fields that you wish to include from the linked records in your API response. By default, only Primary Key and associated display value field is included. + +Example: `fields=["field1","field2"]` or `fields=field1,field2` will include only 'field1' and 'field2' in the API response. | +| `sort` | No | None | Allows you to specify the fields by which you want to sort the records in your API response. Accepts either an array of sort objects or a single sort object. + +Each sort object must have a 'field' property specifying the field name and a 'direction' property with value 'asc' or 'desc'. + +Example: `sort=[{"direction":"asc","field":"field_name"},{"direction":"desc","field":"another_field"}]` or `sort={"direction":"asc","field":"field_name"}` + +If `viewId` query parameter is also included, the sort included here will take precedence over any sorting configuration defined in the view. | +| `where` | No | string | Enables you to define specific conditions for filtering records in your API response. Multiple conditions can be combined using logical operators such as 'and' and 'or'. Each condition consists of three parts: a field name, a comparison operator, and a value. + +Example: `where=(field1,eq,value1)~and(field2,eq,value2)` will filter records where 'field1' is equal to 'value1' AND 'field2' is equal to 'value2'. + +You can also use other comparison operators like 'neq' (not equal), 'gt' (greater than), 'lt' (less than), and more, to create complex filtering rules. + +If `viewId` query parameter is also included, then the filters included here will be applied over the filtering configuration defined in the view. + +Please remember to maintain the specified format, for further information on this please see [the documentation](https://nocodb.com/docs/product-docs/developer-resources/rest-apis#v3-where-query-parameter) | +| `page` | No | integer | Enables you to control the pagination of your API response by specifying the page number you want to retrieve. By default, the first page is returned. If you want to retrieve the next page, you can increment the page number by one. + +Example: `page=2` will return the second page of records in the dataset. | +| `nestedPage` | No | integer | Enables you to control the pagination of your nested data (linked records) in API response by specifying the page number you want to retrieve. By default, the first page is returned. If you want to retrieve the next page, you can increment the page number by one. + +Example: `page=2` will return the second page of nested data records in the dataset. | +| `pageSize` | No | integer | Enables you to set a limit on the number of records you want to retrieve in your API response. By default, your response includes all the available records, but by using this parameter, you can control the quantity you receive. + +Example: `pageSize=100` will constrain your response to the first 100 records in the dataset. | +| `viewId` | No | string | ***View Identifier***. Allows you to fetch records that are currently visible within a specific view. API retrieves records in the order they are displayed if the SORT option is enabled within that view. + +Additionally, if you specify a `sort` query parameter, it will take precedence over any sorting configuration defined in the view. If you specify a `where` query parameter, it will be applied over the filtering configuration defined in the view. + +By default, all fields, including those that are disabled within the view, are included in the response. To explicitly specify which fields to include or exclude, you can use the `fields` query parameter to customize the output according to your requirements. | + +#### Parameter Changes + +**Removed:** `offset`, `limit` +**Added:** `nestedPage`, `page`, `pageSize` + +### List Records Response Schema + +#### v2 Response +``` +- list: array (required) + // List of data objects +- pageInfo: unknown (required) + // Paginated Info +``` + +#### v3 Response +``` +$ref: #/components/schemas/DataListResponseV3 +``` + +### Create Records Request Schema + +#### v2 Request Body +``` +Type: unknown +``` + +#### v3 Request Body +``` +Type: unknown +``` + + +## Table Operations Detailed Analysis + +### Get Table Response Schema + +#### v2 Response +``` +$ref: #/components/schemas/Table +``` + +#### v3 Response +``` +$ref: #/components/schemas/Table +``` + + +--- + +## Key Schema Observations + +### 1. Response Envelope Structure + +Check if both versions use the same response envelope: +- v2: May use `{ list: [...], pageInfo: {...} }` +- v3: May use different structure + +### 2. Error Response Format + +Error responses may differ between versions: +```typescript +// v2 error format (typical) +{ + "msg": "Error message", + "error": "ERROR_CODE" +} + +// v3 error format (may differ) +{ + "message": "Error message", + "statusCode": 400, + "error": "Bad Request" +} +``` + +### 3. Pagination + +Both versions should be checked for: +- Offset/limit based pagination +- Cursor-based pagination +- Page info structure + +### 4. Field Names + +Notable terminology changes: +- `columns` → `fields` +- Check if `Id` vs `id` (capitalization) +- Check timestamp field names diff --git a/docs/NOCODB_API_V2_V3_COMPARISON.md b/docs/NOCODB_API_V2_V3_COMPARISON.md new file mode 100644 index 0000000..a2a817b --- /dev/null +++ b/docs/NOCODB_API_V2_V3_COMPARISON.md @@ -0,0 +1,618 @@ +# NocoDB API v2 to v3 Comprehensive Comparison Report + +**Generated:** 2025-10-10 11:24:00 + +## Table of Contents + +1. [Executive Summary](#executive-summary) +2. [Major Architectural Changes](#major-architectural-changes) +3. [Meta API Detailed Comparison](#meta-api-detailed-comparison) +4. [Data API Detailed Comparison](#data-api-detailed-comparison) +5. [Critical Breaking Changes](#critical-breaking-changes) +6. [Migration Path Analysis](#migration-path-analysis) +7. [Code Migration Examples](#code-migration-examples) +8. [Implementation Strategy](#implementation-strategy) + +## Executive Summary + +### Overview + +NocoDB API v3 represents a **major architectural overhaul** compared to v2: + +- **Meta API**: Dramatically simplified from 137 endpoints to 10 endpoints +- **Data API**: Expanded from 10 endpoints to 36 endpoints with more granular operations +- **Path Structure**: Complete restructuring - v2 paths do NOT directly map to v3 +- **Terminology**: 'columns' → 'fields', simplified resource hierarchy + +### Quick Stats + +| API | v2 Endpoints | v3 Endpoints | Removed | New | Change | +|-----|--------------|--------------|---------|-----|--------| +| **Meta API** | 137 | 10 | 137 | 10 | -93% | +| **Data API** | 10 | 36 | 10 | 36 | +260% | + +--- + +## Major Architectural Changes + +### API Split Strategy + +v3 introduces a clear separation of concerns: + +#### v2 Architecture +``` +Meta API (nocodb-openapi-meta.json): + ├─ Authentication endpoints + ├─ Base/Workspace management + ├─ Table schema operations + ├─ Column operations + ├─ View operations + ├─ Filter/Sort operations + ├─ Webhook operations + └─ Misc utilities + +Data API (nocodb-openapi-data.json): + ├─ Record CRUD (10 endpoints) + └─ File upload +``` + +#### v3 Architecture +``` +Meta API (nocodb-openapi-meta-v3.json): + └─ Data operations ONLY (10 endpoints) + ├─ Record CRUD + ├─ Link operations + └─ File upload + +Data API (nocodb-openapi-data-v3.json): + └─ Meta operations ONLY (36 endpoints) + ├─ Base management + ├─ Table schema + ├─ Field operations (columns) + ├─ View operations + └─ Member management +``` + +**KEY INSIGHT: v2 and v3 have INVERTED their API definitions!** + +- v2's 'Meta API' = v3's 'Data API' (schema/structure) +- v2's 'Data API' = v3's 'Meta API' (records/content) + +### Path Structure Changes + +#### v2 Path Patterns +``` +Authentication: /api/v2/auth/{operation} +Meta Operations: /api/v2/meta/{resource}/{id} +Data Operations: /api/v2/tables/{tableId}/records +Column Operations: /api/v2/meta/columns/{columnId} +View Operations: /api/v2/meta/views/{viewId} +``` + +#### v3 Path Patterns +``` +Meta (Structure): /api/v3/meta/bases/{baseId}/{resource} +Data (Records): /api/v3/data/{baseId}/{tableId}/records +Field Operations: /api/v3/meta/bases/{baseId}/fields/{fieldId} +View Operations: /api/v3/meta/bases/{baseId}/views/{viewId} +Link Operations: /api/v3/data/{baseId}/{tableId}/links/{linkFieldId} +``` + +**KEY CHANGES:** + +1. **baseId is now required** in all paths (was optional/implicit in v2) +2. **Resource hierarchy** is more explicit: `/bases/{baseId}/tables/{tableId}/...` +3. **Terminology change**: `columns` → `fields` +4. **Authentication endpoints removed** from OpenAPI specs (likely moved to separate service) + +--- + +## Critical Breaking Changes + +### 🔴 HIGH PRIORITY: Record Operations + +**Most frequently used endpoints - requires immediate attention** + +| Operation | v2 Endpoint | v3 Endpoint | Breaking Change | +|-----------|-------------|-------------|-----------------| +| List Records | `GET /api/v2/tables/{tableId}/records` | `GET /api/v3/data/{baseId}/{tableId}/records` | **baseId required** | +| Get Record | `GET /api/v2/tables/{tableId}/records/{recordId}` | `GET /api/v3/data/{baseId}/{tableId}/records/{recordId}` | **baseId required** | +| Create Records | `POST /api/v2/tables/{tableId}/records` | `POST /api/v3/data/{baseId}/{tableId}/records` | **baseId required** | +| Update Records | `PATCH /api/v2/tables/{tableId}/records` | `PATCH /api/v3/data/{baseId}/{tableId}/records` | **baseId required** | +| Delete Records | `DELETE /api/v2/tables/{tableId}/records` | `DELETE /api/v3/data/{baseId}/{tableId}/records` | **baseId required** | +| Count Records | `GET /api/v2/tables/{tableId}/records/count` | `GET /api/v3/data/{baseId}/{tableId}/count` | **path change + baseId** | + +### 🟡 MEDIUM PRIORITY: Table & Schema Operations + +| Operation | v2 Endpoint | v3 Endpoint | Breaking Change | +|-----------|-------------|-------------|-----------------| +| List Tables | `GET /api/v2/meta/bases/{baseId}/tables` | `GET /api/v3/meta/bases/{baseId}/tables` | **API file swap** | +| Get Table | `GET /api/v2/meta/tables/{tableId}` | `GET /api/v3/meta/bases/{baseId}/tables/{tableId}` | **baseId required in path** | +| Create Table | `POST /api/v2/meta/bases/{baseId}/tables` | `POST /api/v3/meta/bases/{baseId}/tables` | **API file swap** | +| Update Table | `PATCH /api/v2/meta/tables/{tableId}` | `PATCH /api/v3/meta/bases/{baseId}/tables/{tableId}` | **baseId required in path** | +| Delete Table | `DELETE /api/v2/meta/tables/{tableId}` | `DELETE /api/v3/meta/bases/{baseId}/tables/{tableId}` | **baseId required in path** | + +### 🔵 LOW PRIORITY: Column/Field Operations + +| Operation | v2 Endpoint | v3 Endpoint | Breaking Change | +|-----------|-------------|-------------|-----------------| +| Get Column | `GET /api/v2/meta/columns/{columnId}` | `GET /api/v3/meta/bases/{baseId}/fields/{fieldId}` | **terminology + path change** | +| Update Column | `PATCH /api/v2/meta/columns/{columnId}` | `PATCH /api/v3/meta/bases/{baseId}/fields/{fieldId}` | **terminology + path change** | +| Delete Column | `DELETE /api/v2/meta/columns/{columnId}` | `DELETE /api/v3/meta/bases/{baseId}/fields/{fieldId}` | **terminology + path change** | +| Create Column | `POST /api/v2/meta/tables/{tableId}/columns` | `POST /api/v3/meta/bases/{baseId}/tables/{tableId}/fields` | **terminology + baseId** | + +### 🟣 CRITICAL: Link/Relation Operations + +| Operation | v2 Endpoint | v3 Endpoint | Breaking Change | +|-----------|-------------|-------------|-----------------| +| List Links | `GET /api/v2/tables/{tableId}/links/{linkFieldId}/records/{recordId}` | `GET /api/v3/data/{baseId}/{tableId}/links/{linkFieldId}/{recordId}` | **complete restructure** | +| Link Records | `POST /api/v2/tables/{tableId}/links/{linkFieldId}/records/{recordId}` | `POST /api/v3/data/{baseId}/{tableId}/links/{linkFieldId}/{recordId}` | **complete restructure** | +| Unlink Records | `DELETE /api/v2/tables/{tableId}/links/{linkFieldId}/records/{recordId}` | `DELETE /api/v3/data/{baseId}/{tableId}/links/{linkFieldId}/{recordId}` | **complete restructure** | + +### 🔴 REMOVED: Authentication Endpoints + +These endpoints are completely removed from v3 OpenAPI specs: + +- `POST /api/v2/auth/user/signin` +- `POST /api/v2/auth/user/signup` +- `POST /api/v2/auth/user/signout` +- `GET /api/v2/auth/user/me` +- `POST /api/v2/auth/token/refresh` +- `POST /api/v2/auth/password/forgot` +- `POST /api/v2/auth/password/reset/{token}` +- `POST /api/v2/auth/password/change` + +**Note:** These may still exist but are not documented in the provided v3 OpenAPI specs. + +--- + +## Migration Path Analysis + +### Phase 1: Base ID Resolution + +The biggest structural change is that **baseId is required in all v3 paths**. + +**Challenge:** v2 code often uses just `tableId` without explicitly tracking `baseId`. + +**Solutions:** + +1. **Cache baseId with tableId** when fetching table metadata +2. **Fetch baseId on demand** if not cached +3. **Require baseId** as a parameter in all client methods + +### Phase 2: Endpoint Mapping + +Create adapter layer to map v2 calls to v3: + +```typescript +interface EndpointMapper { + mapRecordsList(tableId: string): { + v2: string; // '/api/v2/tables/{tableId}/records' + v3: string; // '/api/v3/data/{baseId}/{tableId}/records' + }; +} +``` + +### Phase 3: Response Schema Adaptation + +Response structures may have changed. Need to analyze: + +- Field naming conventions +- Nested object structures +- Pagination formats +- Error response formats + +--- + +## Code Migration Examples + +### Example 1: List Records + +**v2 Code:** +```typescript +async function getRecords(tableId: string, params?: QueryParams) { + const response = await fetch( + `${baseUrl}/api/v2/tables/${tableId}/records?${queryString}`, + { headers: { 'xc-token': token } } + ); + return response.json(); +} +``` + +**v3 Code:** +```typescript +async function getRecords( + baseId: string, // ← NEW: baseId required + tableId: string, + params?: QueryParams +) { + const response = await fetch( + `${baseUrl}/api/v3/data/${baseId}/${tableId}/records?${queryString}`, + { headers: { 'xc-token': token } } + ); + return response.json(); +} +``` + +### Example 2: Create Record + +**v2 Code:** +```typescript +async function createRecord(tableId: string, data: Record) { + const response = await fetch( + `${baseUrl}/api/v2/tables/${tableId}/records`, + { + method: 'POST', + headers: { + 'xc-token': token, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + } + ); + return response.json(); +} +``` + +**v3 Code:** +```typescript +async function createRecord( + baseId: string, // ← NEW: baseId required + tableId: string, + data: Record +) { + const response = await fetch( + `${baseUrl}/api/v3/data/${baseId}/${tableId}/records`, + { + method: 'POST', + headers: { + 'xc-token': token, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + } + ); + return response.json(); +} +``` + +### Example 3: Get Table Schema + +**v2 Code:** +```typescript +async function getTable(tableId: string) { + const response = await fetch( + `${baseUrl}/api/v2/meta/tables/${tableId}`, + { headers: { 'xc-token': token } } + ); + return response.json(); +} +``` + +**v3 Code:** +```typescript +async function getTable(baseId: string, tableId: string) { + const response = await fetch( + // NOTE: This is in the 'Data API' file in v3, not 'Meta API' + `${baseUrl}/api/v3/meta/bases/${baseId}/tables/${tableId}`, + { headers: { 'xc-token': token } } + ); + return response.json(); +} +``` + +### Example 4: Link Records + +**v2 Code:** +```typescript +async function linkRecords( + tableId: string, + linkFieldId: string, + recordId: string, + linkedRecordIds: string[] +) { + const response = await fetch( + `${baseUrl}/api/v2/tables/${tableId}/links/${linkFieldId}/records/${recordId}`, + { + method: 'POST', + headers: { + 'xc-token': token, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ linkedRecordIds }) + } + ); + return response.json(); +} +``` + +**v3 Code:** +```typescript +async function linkRecords( + baseId: string, // ← NEW: baseId required + tableId: string, + linkFieldId: string, + recordId: string, + linkedRecordIds: string[] +) { + const response = await fetch( + `${baseUrl}/api/v3/data/${baseId}/${tableId}/links/${linkFieldId}/${recordId}`, + { + method: 'POST', + headers: { + 'xc-token': token, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ linkedRecordIds }) + } + ); + return response.json(); +} +``` + +--- + +## Implementation Strategy + +### 1. Dual-Version Support Architecture + +**Recommended Approach:** Adapter Pattern with Version Detection + +```typescript +// Core interface that both versions implement +interface NocoDBClient { + // Record operations + getRecords(tableId: string, params?: RecordQueryParams): Promise; + getRecord(tableId: string, recordId: string): Promise; + createRecords(tableId: string, records: RecordData[]): Promise; + updateRecords(tableId: string, records: RecordUpdate[]): Promise; + deleteRecords(tableId: string, recordIds: string[]): Promise; + + // Table operations + getTables(baseId: string): Promise; + getTable(tableId: string): Promise; + + // Link operations + linkRecords(tableId: string, linkFieldId: string, recordId: string, linkedIds: string[]): Promise; + unlinkRecords(tableId: string, linkFieldId: string, recordId: string, linkedIds: string[]): Promise; +} + +// Version-specific implementations +class NocoDBClientV2 implements NocoDBClient { + // Implements v2 API paths +} + +class NocoDBClientV3 implements NocoDBClient { + // Implements v3 API paths + // Requires baseId for all operations +} + +// Factory with auto-detection +async function createNocoDBClient(config: ClientConfig): Promise { + const version = await detectApiVersion(config.baseUrl, config.token); + return version === 'v3' + ? new NocoDBClientV3(config) + : new NocoDBClientV2(config); +} +``` + +### 2. Version Detection Strategy + +```typescript +async function detectApiVersion( + baseUrl: string, + token: string +): Promise<'v2' | 'v3'> { + // Option 1: Check for v3-specific endpoint + try { + const response = await fetch( + `${baseUrl}/api/v3/meta/workspaces/`, + { headers: { 'xc-token': token } } + ); + if (response.ok) return 'v3'; + } catch (error) { + // v3 endpoint doesn't exist + } + + // Option 2: Check /api/v2/meta/nocodb/info for version + try { + const response = await fetch( + `${baseUrl}/api/v2/meta/nocodb/info`, + { headers: { 'xc-token': token } } + ); + if (response.ok) { + const info = await response.json(); + // Parse version string to determine API version + return info.version?.startsWith('3.') ? 'v3' : 'v2'; + } + } catch (error) { + // Fallback to v2 + } + + // Default to v2 for backward compatibility + return 'v2'; +} +``` + +### 3. Base ID Resolution Strategy + +Since v3 requires baseId everywhere, implement a resolution mechanism: + +```typescript +class BaseIdResolver { + private cache = new Map(); // tableId -> baseId + + async getBaseIdForTable(tableId: string): Promise { + // Check cache first + if (this.cache.has(tableId)) { + return this.cache.get(tableId)!; + } + + // Fetch all bases and tables to build mapping + const workspaces = await this.listWorkspaces(); + for (const workspace of workspaces) { + const bases = await this.listBases(workspace.id); + for (const base of bases) { + const tables = await this.listTables(base.id); + for (const table of tables) { + this.cache.set(table.id, base.id); + } + } + } + + const baseId = this.cache.get(tableId); + if (!baseId) { + throw new Error(`Could not resolve baseId for tableId: ${tableId}`); + } + return baseId; + } + + // Proactively cache when fetching table metadata + cacheTableBase(tableId: string, baseId: string) { + this.cache.set(tableId, baseId); + } +} +``` + +### 4. Migration Checklist + +#### Phase 1: Foundation (Week 1) +- [ ] Create unified client interface +- [ ] Implement version detection +- [ ] Set up base ID resolver +- [ ] Create v2 adapter implementation +- [ ] Write comprehensive tests + +#### Phase 2: v3 Implementation (Week 2-3) +- [ ] Implement v3 adapter for record operations +- [ ] Implement v3 adapter for table operations +- [ ] Implement v3 adapter for link operations +- [ ] Implement v3 adapter for view operations +- [ ] Handle terminology changes (column → field) + +#### Phase 3: Testing (Week 4) +- [ ] Integration tests against v2 server +- [ ] Integration tests against v3 server +- [ ] Performance benchmarks +- [ ] Error handling verification + +#### Phase 4: Deployment (Week 5) +- [ ] Feature flag for v3 support +- [ ] Gradual rollout strategy +- [ ] Monitoring and alerting +- [ ] Documentation updates + +### 5. Backward Compatibility Strategy + +```typescript +interface ClientConfig { + baseUrl: string; + token: string; + apiVersion?: 'v2' | 'v3' | 'auto'; // Default: 'auto' + baseId?: string; // Optional for v2, required for v3 if not using resolver +} + +class NocoDBClientV3 implements NocoDBClient { + private baseIdResolver: BaseIdResolver; + + async getRecords(tableId: string, params?: RecordQueryParams) { + // Auto-resolve baseId if not provided + const baseId = this.config.baseId || + await this.baseIdResolver.getBaseIdForTable(tableId); + + return this.fetchRecords(baseId, tableId, params); + } +} +``` + +### 6. Key Considerations + +1. **Performance Impact** + - Base ID resolution adds overhead if not cached + - Consider proactive caching during initialization + +2. **Error Handling** + - v3 may return different error structures + - Normalize errors in adapter layer + +3. **Rate Limiting** + - Check if v3 has different rate limits + - Implement appropriate retry logic + +4. **Authentication** + - v3 auth endpoints not in OpenAPI spec + - Verify auth token format compatibility + +5. **Query Parameters** + - Validate that filter/sort syntax is compatible + - Check pagination format (offset/limit vs cursor-based) + +--- + +## Detailed Endpoint Mappings + +### Record Operations Mapping + +| Operation | v2 Path | v3 Path | Notes | +|-----------|---------|---------|-------| +| List | `/api/v2/tables/{tableId}/records` | `/api/v3/data/{baseId}/{tableId}/records` | Add baseId param | +| Get | `/api/v2/tables/{tableId}/records/{recordId}` | `/api/v3/data/{baseId}/{tableId}/records/{recordId}` | Add baseId param | +| Create | `/api/v2/tables/{tableId}/records` | `/api/v3/data/{baseId}/{tableId}/records` | Add baseId param | +| Update | `/api/v2/tables/{tableId}/records` | `/api/v3/data/{baseId}/{tableId}/records` | Add baseId param | +| Delete | `/api/v2/tables/{tableId}/records` | `/api/v3/data/{baseId}/{tableId}/records` | Add baseId param | +| Count | `/api/v2/tables/{tableId}/records/count` | `/api/v3/data/{baseId}/{tableId}/count` | Path structure changed | + +### Table Operations Mapping + +| Operation | v2 Path | v3 Path | Notes | +|-----------|---------|---------|-------| +| List | `/api/v2/meta/bases/{baseId}/tables` | `/api/v3/meta/bases/{baseId}/tables` | Now in 'Data API' spec | +| Get | `/api/v2/meta/tables/{tableId}` | `/api/v3/meta/bases/{baseId}/tables/{tableId}` | Add baseId to path | +| Create | `/api/v2/meta/bases/{baseId}/tables` | `/api/v3/meta/bases/{baseId}/tables` | Now in 'Data API' spec | +| Update | `/api/v2/meta/tables/{tableId}` | `/api/v3/meta/bases/{baseId}/tables/{tableId}` | Add baseId to path | +| Delete | `/api/v2/meta/tables/{tableId}` | `/api/v3/meta/bases/{baseId}/tables/{tableId}` | Add baseId to path | + +### View Operations Mapping + +| Operation | v2 Path | v3 Path | Notes | +|-----------|---------|---------|-------| +| List | `/api/v2/meta/tables/{tableId}/views` | `/api/v3/meta/bases/{baseId}/tables/{tableId}/views` | Add baseId to path | +| Get | `/api/v2/meta/views/{viewId}` (implicit) | `/api/v3/meta/bases/{baseId}/views/{viewId}` | Add baseId to path | +| Create | `/api/v2/meta/tables/{tableId}/grids` (etc) | `/api/v3/meta/bases/{baseId}/tables/{tableId}/views` | Unified view creation | +| Update | `/api/v2/meta/views/{viewId}` | `/api/v3/meta/bases/{baseId}/views/{viewId}` | Add baseId to path | +| Delete | `/api/v2/meta/views/{viewId}` | `/api/v3/meta/bases/{baseId}/views/{viewId}` | Add baseId to path | +| Filters | `/api/v2/meta/views/{viewId}/filters` | `/api/v3/meta/bases/{baseId}/views/{viewId}/filters` | Add baseId to path | +| Sorts | `/api/v2/meta/views/{viewId}/sorts` | `/api/v3/meta/bases/{baseId}/views/{viewId}/sorts` | Add baseId to path | + +### Field/Column Operations Mapping + +| Operation | v2 Path | v3 Path | Notes | +|-----------|---------|---------|-------| +| Get | `/api/v2/meta/columns/{columnId}` | `/api/v3/meta/bases/{baseId}/fields/{fieldId}` | Terminology change + baseId | +| Create | `/api/v2/meta/tables/{tableId}/columns` | `/api/v3/meta/bases/{baseId}/tables/{tableId}/fields` | Terminology change + baseId | +| Update | `/api/v2/meta/columns/{columnId}` | `/api/v3/meta/bases/{baseId}/fields/{fieldId}` | Terminology change + baseId | +| Delete | `/api/v2/meta/columns/{columnId}` | `/api/v3/meta/bases/{baseId}/fields/{fieldId}` | Terminology change + baseId | + +### Link Operations Mapping + +| Operation | v2 Path | v3 Path | Notes | +|-----------|---------|---------|-------| +| List | `/api/v2/tables/{tableId}/links/{linkFieldId}/records/{recordId}` | `/api/v3/data/{baseId}/{tableId}/links/{linkFieldId}/{recordId}` | Complete restructure | +| Link | `/api/v2/tables/{tableId}/links/{linkFieldId}/records/{recordId}` | `/api/v3/data/{baseId}/{tableId}/links/{linkFieldId}/{recordId}` | Complete restructure | +| Unlink | `/api/v2/tables/{tableId}/links/{linkFieldId}/records/{recordId}` | `/api/v3/data/{baseId}/{tableId}/links/{linkFieldId}/{recordId}` | Complete restructure | + +--- + +## Conclusion + +The v2 to v3 migration represents a **major breaking change** that requires: + +1. **Architectural refactoring** - Not just path changes, but structural changes +2. **Base ID management** - New requirement for all operations +3. **API file swap** - Meta/Data definitions inverted +4. **Terminology updates** - columns → fields +5. **Comprehensive testing** - All endpoints need verification + +**Recommended Timeline:** 4-6 weeks for full implementation and testing + +**Risk Level:** HIGH - This is not a simple version bump diff --git a/scripts/migration/analyze_api_detailed.py b/scripts/migration/analyze_api_detailed.py new file mode 100644 index 0000000..d80eb6f --- /dev/null +++ b/scripts/migration/analyze_api_detailed.py @@ -0,0 +1,958 @@ +#!/usr/bin/env python3 +""" +Enhanced comprehensive comparison of NocoDB API v2 vs v3. +Includes detailed schema analysis, migration paths, and code examples. +""" + +import json +import re + + +def load_openapi(filepath: str) -> dict: + """Load OpenAPI JSON file.""" + with open(filepath, encoding="utf-8") as f: + return json.load(f) + + +def get_endpoints(openapi: dict) -> dict[str, dict]: + """Extract all endpoints from OpenAPI spec.""" + endpoints = {} + paths = openapi.get("paths", {}) + for path, methods in paths.items(): + for method, spec in methods.items(): + if method.lower() in ["get", "post", "put", "patch", "delete", "head", "options"]: + key = f"{method.upper()} {path}" + endpoints[key] = { + "path": path, + "method": method.upper(), + "spec": spec, + "summary": spec.get("summary", ""), + "description": spec.get("description", ""), + "operationId": spec.get("operationId", ""), + "tags": spec.get("tags", []), + "parameters": spec.get("parameters", []), + "requestBody": spec.get("requestBody", {}), + "responses": spec.get("responses", {}), + "security": spec.get("security", []), + } + return endpoints + + +def extract_path_params(path: str) -> list[str]: + """Extract parameter names from path.""" + return re.findall(r"\{([^}]+)\}", path) + + +def find_path_mapping(v2_path: str, v3_endpoints: dict) -> list[str]: + """Find potential v3 path mappings for a v2 path.""" + v2_params = extract_path_params(v2_path) + v2_parts = [p for p in v2_path.split("/") if p and not p.startswith("{")] + + candidates = [] + for v3_key, v3_data in v3_endpoints.items(): + v3_path = v3_data["path"] + v3_params = extract_path_params(v3_path) + v3_parts = [p for p in v3_path.split("/") if p and not p.startswith("{")] + + # Check if similar structure + if len(v2_params) == len(v3_params): + common_parts = set(v2_parts) & set(v3_parts) + if len(common_parts) >= min(len(v2_parts), len(v3_parts)) * 0.5: + candidates.append(v3_key) + + return candidates + + +def analyze_migration_path(v2_endpoint: str, v2_data: dict, v3_endpoints: dict) -> dict: + """Analyze migration path from v2 to v3.""" + method, path = v2_endpoint.split(" ", 1) + + # Try to find direct v3 equivalent + v3_candidates = find_path_mapping(path, v3_endpoints) + + migration = { + "v2_endpoint": v2_endpoint, + "v2_summary": v2_data.get("summary", ""), + "v3_candidates": [], + "migration_complexity": "unknown", + "recommendations": [], + } + + for candidate in v3_candidates: + cand_method, cand_path = candidate.split(" ", 1) + if cand_method == method: + migration["v3_candidates"].append(candidate) + + if not migration["v3_candidates"]: + migration["migration_complexity"] = "high" + migration["recommendations"].append( + "No direct v3 equivalent found. May require restructured approach." + ) + elif len(migration["v3_candidates"]) == 1: + migration["migration_complexity"] = "low" + migration["recommendations"].append("Direct 1:1 mapping available with path changes.") + else: + migration["migration_complexity"] = "medium" + migration["recommendations"].append( + "Multiple potential mappings. Requires careful analysis." + ) + + return migration + + +def generate_detailed_markdown(meta_analysis: dict, data_analysis: dict) -> str: + """Generate comprehensive markdown with detailed analysis.""" + + md = [] + md.append("# NocoDB API v2 to v3 Comprehensive Comparison Report") + md.append("") + md.append( + f"**Generated:** {__import__('datetime').datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + md.append("") + md.append("## Table of Contents") + md.append("") + md.append("1. [Executive Summary](#executive-summary)") + md.append("2. [Major Architectural Changes](#major-architectural-changes)") + md.append("3. [Meta API Detailed Comparison](#meta-api-detailed-comparison)") + md.append("4. [Data API Detailed Comparison](#data-api-detailed-comparison)") + md.append("5. [Critical Breaking Changes](#critical-breaking-changes)") + md.append("6. [Migration Path Analysis](#migration-path-analysis)") + md.append("7. [Code Migration Examples](#code-migration-examples)") + md.append("8. [Implementation Strategy](#implementation-strategy)") + md.append("") + + # Executive Summary + md.append("## Executive Summary") + md.append("") + md.append("### Overview") + md.append("") + md.append("NocoDB API v3 represents a **major architectural overhaul** compared to v2:") + md.append("") + md.append("- **Meta API**: Dramatically simplified from 137 endpoints to 10 endpoints") + md.append( + "- **Data API**: Expanded from 10 endpoints to 36 endpoints with more granular operations" + ) + md.append("- **Path Structure**: Complete restructuring - v2 paths do NOT directly map to v3") + md.append("- **Terminology**: 'columns' → 'fields', simplified resource hierarchy") + md.append("") + + meta_stats = { + "removed": len(meta_analysis["comparison"]["removed"]), + "new": len(meta_analysis["comparison"]["new"]), + } + + data_stats = { + "removed": len(data_analysis["comparison"]["removed"]), + "new": len(data_analysis["comparison"]["new"]), + } + + md.append("### Quick Stats") + md.append("") + md.append("| API | v2 Endpoints | v3 Endpoints | Removed | New | Change |") + md.append("|-----|--------------|--------------|---------|-----|--------|") + md.append(f"| **Meta API** | 137 | 10 | {meta_stats['removed']} | {meta_stats['new']} | -93% |") + md.append(f"| **Data API** | 10 | 36 | {data_stats['removed']} | {data_stats['new']} | +260% |") + md.append("") + + # Major Architectural Changes + md.append("---") + md.append("") + md.append("## Major Architectural Changes") + md.append("") + md.append("### API Split Strategy") + md.append("") + md.append("v3 introduces a clear separation of concerns:") + md.append("") + md.append("#### v2 Architecture") + md.append("```") + md.append("Meta API (nocodb-openapi-meta.json):") + md.append(" ├─ Authentication endpoints") + md.append(" ├─ Base/Workspace management") + md.append(" ├─ Table schema operations") + md.append(" ├─ Column operations") + md.append(" ├─ View operations") + md.append(" ├─ Filter/Sort operations") + md.append(" ├─ Webhook operations") + md.append(" └─ Misc utilities") + md.append("") + md.append("Data API (nocodb-openapi-data.json):") + md.append(" ├─ Record CRUD (10 endpoints)") + md.append(" └─ File upload") + md.append("```") + md.append("") + md.append("#### v3 Architecture") + md.append("```") + md.append("Meta API (nocodb-openapi-meta-v3.json):") + md.append(" └─ Data operations ONLY (10 endpoints)") + md.append(" ├─ Record CRUD") + md.append(" ├─ Link operations") + md.append(" └─ File upload") + md.append("") + md.append("Data API (nocodb-openapi-data-v3.json):") + md.append(" └─ Meta operations ONLY (36 endpoints)") + md.append(" ├─ Base management") + md.append(" ├─ Table schema") + md.append(" ├─ Field operations (columns)") + md.append(" ├─ View operations") + md.append(" └─ Member management") + md.append("```") + md.append("") + md.append("**KEY INSIGHT: v2 and v3 have INVERTED their API definitions!**") + md.append("") + md.append("- v2's 'Meta API' = v3's 'Data API' (schema/structure)") + md.append("- v2's 'Data API' = v3's 'Meta API' (records/content)") + md.append("") + + # Path Structure Changes + md.append("### Path Structure Changes") + md.append("") + md.append("#### v2 Path Patterns") + md.append("```") + md.append("Authentication: /api/v2/auth/{operation}") + md.append("Meta Operations: /api/v2/meta/{resource}/{id}") + md.append("Data Operations: /api/v2/tables/{tableId}/records") + md.append("Column Operations: /api/v2/meta/columns/{columnId}") + md.append("View Operations: /api/v2/meta/views/{viewId}") + md.append("```") + md.append("") + md.append("#### v3 Path Patterns") + md.append("```") + md.append("Meta (Structure): /api/v3/meta/bases/{baseId}/{resource}") + md.append("Data (Records): /api/v3/data/{baseId}/{tableId}/records") + md.append("Field Operations: /api/v3/meta/bases/{baseId}/fields/{fieldId}") + md.append("View Operations: /api/v3/meta/bases/{baseId}/views/{viewId}") + md.append("Link Operations: /api/v3/data/{baseId}/{tableId}/links/{linkFieldId}") + md.append("```") + md.append("") + md.append("**KEY CHANGES:**") + md.append("") + md.append("1. **baseId is now required** in all paths (was optional/implicit in v2)") + md.append("2. **Resource hierarchy** is more explicit: `/bases/{baseId}/tables/{tableId}/...`") + md.append("3. **Terminology change**: `columns` → `fields`") + md.append( + "4. **Authentication endpoints removed** from OpenAPI specs (likely moved to separate service)" + ) + md.append("") + + # Critical Breaking Changes + md.append("---") + md.append("") + md.append("## Critical Breaking Changes") + md.append("") + md.append("### 🔴 HIGH PRIORITY: Record Operations") + md.append("") + md.append("**Most frequently used endpoints - requires immediate attention**") + md.append("") + md.append("| Operation | v2 Endpoint | v3 Endpoint | Breaking Change |") + md.append("|-----------|-------------|-------------|-----------------|") + md.append( + "| List Records | `GET /api/v2/tables/{tableId}/records` | `GET /api/v3/data/{baseId}/{tableId}/records` | **baseId required** |" + ) + md.append( + "| Get Record | `GET /api/v2/tables/{tableId}/records/{recordId}` | `GET /api/v3/data/{baseId}/{tableId}/records/{recordId}` | **baseId required** |" + ) + md.append( + "| Create Records | `POST /api/v2/tables/{tableId}/records` | `POST /api/v3/data/{baseId}/{tableId}/records` | **baseId required** |" + ) + md.append( + "| Update Records | `PATCH /api/v2/tables/{tableId}/records` | `PATCH /api/v3/data/{baseId}/{tableId}/records` | **baseId required** |" + ) + md.append( + "| Delete Records | `DELETE /api/v2/tables/{tableId}/records` | `DELETE /api/v3/data/{baseId}/{tableId}/records` | **baseId required** |" + ) + md.append( + "| Count Records | `GET /api/v2/tables/{tableId}/records/count` | `GET /api/v3/data/{baseId}/{tableId}/count` | **path change + baseId** |" + ) + md.append("") + + md.append("### 🟡 MEDIUM PRIORITY: Table & Schema Operations") + md.append("") + md.append("| Operation | v2 Endpoint | v3 Endpoint | Breaking Change |") + md.append("|-----------|-------------|-------------|-----------------|") + md.append( + "| List Tables | `GET /api/v2/meta/bases/{baseId}/tables` | `GET /api/v3/meta/bases/{baseId}/tables` | **API file swap** |" + ) + md.append( + "| Get Table | `GET /api/v2/meta/tables/{tableId}` | `GET /api/v3/meta/bases/{baseId}/tables/{tableId}` | **baseId required in path** |" + ) + md.append( + "| Create Table | `POST /api/v2/meta/bases/{baseId}/tables` | `POST /api/v3/meta/bases/{baseId}/tables` | **API file swap** |" + ) + md.append( + "| Update Table | `PATCH /api/v2/meta/tables/{tableId}` | `PATCH /api/v3/meta/bases/{baseId}/tables/{tableId}` | **baseId required in path** |" + ) + md.append( + "| Delete Table | `DELETE /api/v2/meta/tables/{tableId}` | `DELETE /api/v3/meta/bases/{baseId}/tables/{tableId}` | **baseId required in path** |" + ) + md.append("") + + md.append("### 🔵 LOW PRIORITY: Column/Field Operations") + md.append("") + md.append("| Operation | v2 Endpoint | v3 Endpoint | Breaking Change |") + md.append("|-----------|-------------|-------------|-----------------|") + md.append( + "| Get Column | `GET /api/v2/meta/columns/{columnId}` | `GET /api/v3/meta/bases/{baseId}/fields/{fieldId}` | **terminology + path change** |" + ) + md.append( + "| Update Column | `PATCH /api/v2/meta/columns/{columnId}` | `PATCH /api/v3/meta/bases/{baseId}/fields/{fieldId}` | **terminology + path change** |" + ) + md.append( + "| Delete Column | `DELETE /api/v2/meta/columns/{columnId}` | `DELETE /api/v3/meta/bases/{baseId}/fields/{fieldId}` | **terminology + path change** |" + ) + md.append( + "| Create Column | `POST /api/v2/meta/tables/{tableId}/columns` | `POST /api/v3/meta/bases/{baseId}/tables/{tableId}/fields` | **terminology + baseId** |" + ) + md.append("") + + md.append("### 🟣 CRITICAL: Link/Relation Operations") + md.append("") + md.append("| Operation | v2 Endpoint | v3 Endpoint | Breaking Change |") + md.append("|-----------|-------------|-------------|-----------------|") + md.append( + "| List Links | `GET /api/v2/tables/{tableId}/links/{linkFieldId}/records/{recordId}` | `GET /api/v3/data/{baseId}/{tableId}/links/{linkFieldId}/{recordId}` | **complete restructure** |" + ) + md.append( + "| Link Records | `POST /api/v2/tables/{tableId}/links/{linkFieldId}/records/{recordId}` | `POST /api/v3/data/{baseId}/{tableId}/links/{linkFieldId}/{recordId}` | **complete restructure** |" + ) + md.append( + "| Unlink Records | `DELETE /api/v2/tables/{tableId}/links/{linkFieldId}/records/{recordId}` | `DELETE /api/v3/data/{baseId}/{tableId}/links/{linkFieldId}/{recordId}` | **complete restructure** |" + ) + md.append("") + + md.append("### 🔴 REMOVED: Authentication Endpoints") + md.append("") + md.append("These endpoints are completely removed from v3 OpenAPI specs:") + md.append("") + md.append("- `POST /api/v2/auth/user/signin`") + md.append("- `POST /api/v2/auth/user/signup`") + md.append("- `POST /api/v2/auth/user/signout`") + md.append("- `GET /api/v2/auth/user/me`") + md.append("- `POST /api/v2/auth/token/refresh`") + md.append("- `POST /api/v2/auth/password/forgot`") + md.append("- `POST /api/v2/auth/password/reset/{token}`") + md.append("- `POST /api/v2/auth/password/change`") + md.append("") + md.append( + "**Note:** These may still exist but are not documented in the provided v3 OpenAPI specs." + ) + md.append("") + + # Migration Path Analysis + md.append("---") + md.append("") + md.append("## Migration Path Analysis") + md.append("") + md.append("### Phase 1: Base ID Resolution") + md.append("") + md.append("The biggest structural change is that **baseId is required in all v3 paths**.") + md.append("") + md.append( + "**Challenge:** v2 code often uses just `tableId` without explicitly tracking `baseId`." + ) + md.append("") + md.append("**Solutions:**") + md.append("") + md.append("1. **Cache baseId with tableId** when fetching table metadata") + md.append("2. **Fetch baseId on demand** if not cached") + md.append("3. **Require baseId** as a parameter in all client methods") + md.append("") + + md.append("### Phase 2: Endpoint Mapping") + md.append("") + md.append("Create adapter layer to map v2 calls to v3:") + md.append("") + md.append("```typescript") + md.append("interface EndpointMapper {") + md.append(" mapRecordsList(tableId: string): {") + md.append(" v2: string; // '/api/v2/tables/{tableId}/records'") + md.append(" v3: string; // '/api/v3/data/{baseId}/{tableId}/records'") + md.append(" };") + md.append("}") + md.append("```") + md.append("") + + md.append("### Phase 3: Response Schema Adaptation") + md.append("") + md.append("Response structures may have changed. Need to analyze:") + md.append("") + md.append("- Field naming conventions") + md.append("- Nested object structures") + md.append("- Pagination formats") + md.append("- Error response formats") + md.append("") + + # Code Migration Examples + md.append("---") + md.append("") + md.append("## Code Migration Examples") + md.append("") + + md.append("### Example 1: List Records") + md.append("") + md.append("**v2 Code:**") + md.append("```typescript") + md.append("async function getRecords(tableId: string, params?: QueryParams) {") + md.append(" const response = await fetch(") + md.append(" `${baseUrl}/api/v2/tables/${tableId}/records?${queryString}`,") + md.append(" { headers: { 'xc-token': token } }") + md.append(" );") + md.append(" return response.json();") + md.append("}") + md.append("```") + md.append("") + md.append("**v3 Code:**") + md.append("```typescript") + md.append("async function getRecords(") + md.append(" baseId: string, // ← NEW: baseId required") + md.append(" tableId: string,") + md.append(" params?: QueryParams") + md.append(") {") + md.append(" const response = await fetch(") + md.append(" `${baseUrl}/api/v3/data/${baseId}/${tableId}/records?${queryString}`,") + md.append(" { headers: { 'xc-token': token } }") + md.append(" );") + md.append(" return response.json();") + md.append("}") + md.append("```") + md.append("") + + md.append("### Example 2: Create Record") + md.append("") + md.append("**v2 Code:**") + md.append("```typescript") + md.append("async function createRecord(tableId: string, data: Record) {") + md.append(" const response = await fetch(") + md.append(" `${baseUrl}/api/v2/tables/${tableId}/records`,") + md.append(" {") + md.append(" method: 'POST',") + md.append(" headers: {") + md.append(" 'xc-token': token,") + md.append(" 'Content-Type': 'application/json'") + md.append(" },") + md.append(" body: JSON.stringify(data)") + md.append(" }") + md.append(" );") + md.append(" return response.json();") + md.append("}") + md.append("```") + md.append("") + md.append("**v3 Code:**") + md.append("```typescript") + md.append("async function createRecord(") + md.append(" baseId: string, // ← NEW: baseId required") + md.append(" tableId: string,") + md.append(" data: Record") + md.append(") {") + md.append(" const response = await fetch(") + md.append(" `${baseUrl}/api/v3/data/${baseId}/${tableId}/records`,") + md.append(" {") + md.append(" method: 'POST',") + md.append(" headers: {") + md.append(" 'xc-token': token,") + md.append(" 'Content-Type': 'application/json'") + md.append(" },") + md.append(" body: JSON.stringify(data)") + md.append(" }") + md.append(" );") + md.append(" return response.json();") + md.append("}") + md.append("```") + md.append("") + + md.append("### Example 3: Get Table Schema") + md.append("") + md.append("**v2 Code:**") + md.append("```typescript") + md.append("async function getTable(tableId: string) {") + md.append(" const response = await fetch(") + md.append(" `${baseUrl}/api/v2/meta/tables/${tableId}`,") + md.append(" { headers: { 'xc-token': token } }") + md.append(" );") + md.append(" return response.json();") + md.append("}") + md.append("```") + md.append("") + md.append("**v3 Code:**") + md.append("```typescript") + md.append("async function getTable(baseId: string, tableId: string) {") + md.append(" const response = await fetch(") + md.append(" // NOTE: This is in the 'Data API' file in v3, not 'Meta API'") + md.append(" `${baseUrl}/api/v3/meta/bases/${baseId}/tables/${tableId}`,") + md.append(" { headers: { 'xc-token': token } }") + md.append(" );") + md.append(" return response.json();") + md.append("}") + md.append("```") + md.append("") + + md.append("### Example 4: Link Records") + md.append("") + md.append("**v2 Code:**") + md.append("```typescript") + md.append("async function linkRecords(") + md.append(" tableId: string,") + md.append(" linkFieldId: string,") + md.append(" recordId: string,") + md.append(" linkedRecordIds: string[]") + md.append(") {") + md.append(" const response = await fetch(") + md.append(" `${baseUrl}/api/v2/tables/${tableId}/links/${linkFieldId}/records/${recordId}`,") + md.append(" {") + md.append(" method: 'POST',") + md.append(" headers: {") + md.append(" 'xc-token': token,") + md.append(" 'Content-Type': 'application/json'") + md.append(" },") + md.append(" body: JSON.stringify({ linkedRecordIds })") + md.append(" }") + md.append(" );") + md.append(" return response.json();") + md.append("}") + md.append("```") + md.append("") + md.append("**v3 Code:**") + md.append("```typescript") + md.append("async function linkRecords(") + md.append(" baseId: string, // ← NEW: baseId required") + md.append(" tableId: string,") + md.append(" linkFieldId: string,") + md.append(" recordId: string,") + md.append(" linkedRecordIds: string[]") + md.append(") {") + md.append(" const response = await fetch(") + md.append(" `${baseUrl}/api/v3/data/${baseId}/${tableId}/links/${linkFieldId}/${recordId}`,") + md.append(" {") + md.append(" method: 'POST',") + md.append(" headers: {") + md.append(" 'xc-token': token,") + md.append(" 'Content-Type': 'application/json'") + md.append(" },") + md.append(" body: JSON.stringify({ linkedRecordIds })") + md.append(" }") + md.append(" );") + md.append(" return response.json();") + md.append("}") + md.append("```") + md.append("") + + # Implementation Strategy + md.append("---") + md.append("") + md.append("## Implementation Strategy") + md.append("") + + md.append("### 1. Dual-Version Support Architecture") + md.append("") + md.append("**Recommended Approach:** Adapter Pattern with Version Detection") + md.append("") + md.append("```typescript") + md.append("// Core interface that both versions implement") + md.append("interface NocoDBClient {") + md.append(" // Record operations") + md.append(" getRecords(tableId: string, params?: RecordQueryParams): Promise;") + md.append(" getRecord(tableId: string, recordId: string): Promise;") + md.append(" createRecords(tableId: string, records: RecordData[]): Promise;") + md.append(" updateRecords(tableId: string, records: RecordUpdate[]): Promise;") + md.append(" deleteRecords(tableId: string, recordIds: string[]): Promise;") + md.append(" ") + md.append(" // Table operations") + md.append(" getTables(baseId: string): Promise;") + md.append(" getTable(tableId: string): Promise
;") + md.append(" ") + md.append(" // Link operations") + md.append( + " linkRecords(tableId: string, linkFieldId: string, recordId: string, linkedIds: string[]): Promise;" + ) + md.append( + " unlinkRecords(tableId: string, linkFieldId: string, recordId: string, linkedIds: string[]): Promise;" + ) + md.append("}") + md.append("") + md.append("// Version-specific implementations") + md.append("class NocoDBClientV2 implements NocoDBClient {") + md.append(" // Implements v2 API paths") + md.append("}") + md.append("") + md.append("class NocoDBClientV3 implements NocoDBClient {") + md.append(" // Implements v3 API paths") + md.append(" // Requires baseId for all operations") + md.append("}") + md.append("") + md.append("// Factory with auto-detection") + md.append("async function createNocoDBClient(config: ClientConfig): Promise {") + md.append(" const version = await detectApiVersion(config.baseUrl, config.token);") + md.append(" return version === 'v3' ") + md.append(" ? new NocoDBClientV3(config)") + md.append(" : new NocoDBClientV2(config);") + md.append("}") + md.append("```") + md.append("") + + md.append("### 2. Version Detection Strategy") + md.append("") + md.append("```typescript") + md.append("async function detectApiVersion(") + md.append(" baseUrl: string,") + md.append(" token: string") + md.append("): Promise<'v2' | 'v3'> {") + md.append(" // Option 1: Check for v3-specific endpoint") + md.append(" try {") + md.append(" const response = await fetch(") + md.append(" `${baseUrl}/api/v3/meta/workspaces/`,") + md.append(" { headers: { 'xc-token': token } }") + md.append(" );") + md.append(" if (response.ok) return 'v3';") + md.append(" } catch (error) {") + md.append(" // v3 endpoint doesn't exist") + md.append(" }") + md.append(" ") + md.append(" // Option 2: Check /api/v2/meta/nocodb/info for version") + md.append(" try {") + md.append(" const response = await fetch(") + md.append(" `${baseUrl}/api/v2/meta/nocodb/info`,") + md.append(" { headers: { 'xc-token': token } }") + md.append(" );") + md.append(" if (response.ok) {") + md.append(" const info = await response.json();") + md.append(" // Parse version string to determine API version") + md.append(" return info.version?.startsWith('3.') ? 'v3' : 'v2';") + md.append(" }") + md.append(" } catch (error) {") + md.append(" // Fallback to v2") + md.append(" }") + md.append(" ") + md.append(" // Default to v2 for backward compatibility") + md.append(" return 'v2';") + md.append("}") + md.append("```") + md.append("") + + md.append("### 3. Base ID Resolution Strategy") + md.append("") + md.append("Since v3 requires baseId everywhere, implement a resolution mechanism:") + md.append("") + md.append("```typescript") + md.append("class BaseIdResolver {") + md.append(" private cache = new Map(); // tableId -> baseId") + md.append(" ") + md.append(" async getBaseIdForTable(tableId: string): Promise {") + md.append(" // Check cache first") + md.append(" if (this.cache.has(tableId)) {") + md.append(" return this.cache.get(tableId)!;") + md.append(" }") + md.append(" ") + md.append(" // Fetch all bases and tables to build mapping") + md.append(" const workspaces = await this.listWorkspaces();") + md.append(" for (const workspace of workspaces) {") + md.append(" const bases = await this.listBases(workspace.id);") + md.append(" for (const base of bases) {") + md.append(" const tables = await this.listTables(base.id);") + md.append(" for (const table of tables) {") + md.append(" this.cache.set(table.id, base.id);") + md.append(" }") + md.append(" }") + md.append(" }") + md.append(" ") + md.append(" const baseId = this.cache.get(tableId);") + md.append(" if (!baseId) {") + md.append(" throw new Error(`Could not resolve baseId for tableId: ${tableId}`);") + md.append(" }") + md.append(" return baseId;") + md.append(" }") + md.append(" ") + md.append(" // Proactively cache when fetching table metadata") + md.append(" cacheTableBase(tableId: string, baseId: string) {") + md.append(" this.cache.set(tableId, baseId);") + md.append(" }") + md.append("}") + md.append("```") + md.append("") + + md.append("### 4. Migration Checklist") + md.append("") + md.append("#### Phase 1: Foundation (Week 1)") + md.append("- [ ] Create unified client interface") + md.append("- [ ] Implement version detection") + md.append("- [ ] Set up base ID resolver") + md.append("- [ ] Create v2 adapter implementation") + md.append("- [ ] Write comprehensive tests") + md.append("") + md.append("#### Phase 2: v3 Implementation (Week 2-3)") + md.append("- [ ] Implement v3 adapter for record operations") + md.append("- [ ] Implement v3 adapter for table operations") + md.append("- [ ] Implement v3 adapter for link operations") + md.append("- [ ] Implement v3 adapter for view operations") + md.append("- [ ] Handle terminology changes (column → field)") + md.append("") + md.append("#### Phase 3: Testing (Week 4)") + md.append("- [ ] Integration tests against v2 server") + md.append("- [ ] Integration tests against v3 server") + md.append("- [ ] Performance benchmarks") + md.append("- [ ] Error handling verification") + md.append("") + md.append("#### Phase 4: Deployment (Week 5)") + md.append("- [ ] Feature flag for v3 support") + md.append("- [ ] Gradual rollout strategy") + md.append("- [ ] Monitoring and alerting") + md.append("- [ ] Documentation updates") + md.append("") + + md.append("### 5. Backward Compatibility Strategy") + md.append("") + md.append("```typescript") + md.append("interface ClientConfig {") + md.append(" baseUrl: string;") + md.append(" token: string;") + md.append(" apiVersion?: 'v2' | 'v3' | 'auto'; // Default: 'auto'") + md.append(" baseId?: string; // Optional for v2, required for v3 if not using resolver") + md.append("}") + md.append("") + md.append("class NocoDBClientV3 implements NocoDBClient {") + md.append(" private baseIdResolver: BaseIdResolver;") + md.append(" ") + md.append(" async getRecords(tableId: string, params?: RecordQueryParams) {") + md.append(" // Auto-resolve baseId if not provided") + md.append(" const baseId = this.config.baseId || ") + md.append(" await this.baseIdResolver.getBaseIdForTable(tableId);") + md.append(" ") + md.append(" return this.fetchRecords(baseId, tableId, params);") + md.append(" }") + md.append("}") + md.append("```") + md.append("") + + md.append("### 6. Key Considerations") + md.append("") + md.append("1. **Performance Impact**") + md.append(" - Base ID resolution adds overhead if not cached") + md.append(" - Consider proactive caching during initialization") + md.append("") + md.append("2. **Error Handling**") + md.append(" - v3 may return different error structures") + md.append(" - Normalize errors in adapter layer") + md.append("") + md.append("3. **Rate Limiting**") + md.append(" - Check if v3 has different rate limits") + md.append(" - Implement appropriate retry logic") + md.append("") + md.append("4. **Authentication**") + md.append(" - v3 auth endpoints not in OpenAPI spec") + md.append(" - Verify auth token format compatibility") + md.append("") + md.append("5. **Query Parameters**") + md.append(" - Validate that filter/sort syntax is compatible") + md.append(" - Check pagination format (offset/limit vs cursor-based)") + md.append("") + + md.append("---") + md.append("") + md.append("## Detailed Endpoint Mappings") + md.append("") + + # Generate detailed mapping tables + md.extend(generate_mapping_tables()) + + md.append("---") + md.append("") + md.append("## Conclusion") + md.append("") + md.append("The v2 to v3 migration represents a **major breaking change** that requires:") + md.append("") + md.append("1. **Architectural refactoring** - Not just path changes, but structural changes") + md.append("2. **Base ID management** - New requirement for all operations") + md.append("3. **API file swap** - Meta/Data definitions inverted") + md.append("4. **Terminology updates** - columns → fields") + md.append("5. **Comprehensive testing** - All endpoints need verification") + md.append("") + md.append("**Recommended Timeline:** 4-6 weeks for full implementation and testing") + md.append("") + md.append("**Risk Level:** HIGH - This is not a simple version bump") + md.append("") + + return "\n".join(md) + + +def generate_mapping_tables() -> list[str]: + """Generate detailed mapping tables.""" + md = [] + + md.append("### Record Operations Mapping") + md.append("") + md.append("| Operation | v2 Path | v3 Path | Notes |") + md.append("|-----------|---------|---------|-------|") + md.append( + "| List | `/api/v2/tables/{tableId}/records` | `/api/v3/data/{baseId}/{tableId}/records` | Add baseId param |" + ) + md.append( + "| Get | `/api/v2/tables/{tableId}/records/{recordId}` | `/api/v3/data/{baseId}/{tableId}/records/{recordId}` | Add baseId param |" + ) + md.append( + "| Create | `/api/v2/tables/{tableId}/records` | `/api/v3/data/{baseId}/{tableId}/records` | Add baseId param |" + ) + md.append( + "| Update | `/api/v2/tables/{tableId}/records` | `/api/v3/data/{baseId}/{tableId}/records` | Add baseId param |" + ) + md.append( + "| Delete | `/api/v2/tables/{tableId}/records` | `/api/v3/data/{baseId}/{tableId}/records` | Add baseId param |" + ) + md.append( + "| Count | `/api/v2/tables/{tableId}/records/count` | `/api/v3/data/{baseId}/{tableId}/count` | Path structure changed |" + ) + md.append("") + + md.append("### Table Operations Mapping") + md.append("") + md.append("| Operation | v2 Path | v3 Path | Notes |") + md.append("|-----------|---------|---------|-------|") + md.append( + "| List | `/api/v2/meta/bases/{baseId}/tables` | `/api/v3/meta/bases/{baseId}/tables` | Now in 'Data API' spec |" + ) + md.append( + "| Get | `/api/v2/meta/tables/{tableId}` | `/api/v3/meta/bases/{baseId}/tables/{tableId}` | Add baseId to path |" + ) + md.append( + "| Create | `/api/v2/meta/bases/{baseId}/tables` | `/api/v3/meta/bases/{baseId}/tables` | Now in 'Data API' spec |" + ) + md.append( + "| Update | `/api/v2/meta/tables/{tableId}` | `/api/v3/meta/bases/{baseId}/tables/{tableId}` | Add baseId to path |" + ) + md.append( + "| Delete | `/api/v2/meta/tables/{tableId}` | `/api/v3/meta/bases/{baseId}/tables/{tableId}` | Add baseId to path |" + ) + md.append("") + + md.append("### View Operations Mapping") + md.append("") + md.append("| Operation | v2 Path | v3 Path | Notes |") + md.append("|-----------|---------|---------|-------|") + md.append( + "| List | `/api/v2/meta/tables/{tableId}/views` | `/api/v3/meta/bases/{baseId}/tables/{tableId}/views` | Add baseId to path |" + ) + md.append( + "| Get | `/api/v2/meta/views/{viewId}` (implicit) | `/api/v3/meta/bases/{baseId}/views/{viewId}` | Add baseId to path |" + ) + md.append( + "| Create | `/api/v2/meta/tables/{tableId}/grids` (etc) | `/api/v3/meta/bases/{baseId}/tables/{tableId}/views` | Unified view creation |" + ) + md.append( + "| Update | `/api/v2/meta/views/{viewId}` | `/api/v3/meta/bases/{baseId}/views/{viewId}` | Add baseId to path |" + ) + md.append( + "| Delete | `/api/v2/meta/views/{viewId}` | `/api/v3/meta/bases/{baseId}/views/{viewId}` | Add baseId to path |" + ) + md.append( + "| Filters | `/api/v2/meta/views/{viewId}/filters` | `/api/v3/meta/bases/{baseId}/views/{viewId}/filters` | Add baseId to path |" + ) + md.append( + "| Sorts | `/api/v2/meta/views/{viewId}/sorts` | `/api/v3/meta/bases/{baseId}/views/{viewId}/sorts` | Add baseId to path |" + ) + md.append("") + + md.append("### Field/Column Operations Mapping") + md.append("") + md.append("| Operation | v2 Path | v3 Path | Notes |") + md.append("|-----------|---------|---------|-------|") + md.append( + "| Get | `/api/v2/meta/columns/{columnId}` | `/api/v3/meta/bases/{baseId}/fields/{fieldId}` | Terminology change + baseId |" + ) + md.append( + "| Create | `/api/v2/meta/tables/{tableId}/columns` | `/api/v3/meta/bases/{baseId}/tables/{tableId}/fields` | Terminology change + baseId |" + ) + md.append( + "| Update | `/api/v2/meta/columns/{columnId}` | `/api/v3/meta/bases/{baseId}/fields/{fieldId}` | Terminology change + baseId |" + ) + md.append( + "| Delete | `/api/v2/meta/columns/{columnId}` | `/api/v3/meta/bases/{baseId}/fields/{fieldId}` | Terminology change + baseId |" + ) + md.append("") + + md.append("### Link Operations Mapping") + md.append("") + md.append("| Operation | v2 Path | v3 Path | Notes |") + md.append("|-----------|---------|---------|-------|") + md.append( + "| List | `/api/v2/tables/{tableId}/links/{linkFieldId}/records/{recordId}` | `/api/v3/data/{baseId}/{tableId}/links/{linkFieldId}/{recordId}` | Complete restructure |" + ) + md.append( + "| Link | `/api/v2/tables/{tableId}/links/{linkFieldId}/records/{recordId}` | `/api/v3/data/{baseId}/{tableId}/links/{linkFieldId}/{recordId}` | Complete restructure |" + ) + md.append( + "| Unlink | `/api/v2/tables/{tableId}/links/{linkFieldId}/records/{recordId}` | `/api/v3/data/{baseId}/{tableId}/links/{linkFieldId}/{recordId}` | Complete restructure |" + ) + md.append("") + + return md + + +def main(): + print("Loading OpenAPI specifications...") + + # Load files + meta_v2 = load_openapi("docs/nocodb-openapi-meta.json") + meta_v3 = load_openapi("docs/nocodb-openapi-meta-v3.json") + data_v2 = load_openapi("docs/nocodb-openapi-data.json") + data_v3 = load_openapi("docs/nocodb-openapi-data-v3.json") + + print("Extracting endpoints...") + + # Extract endpoints + meta_v2_endpoints = get_endpoints(meta_v2) + meta_v3_endpoints = get_endpoints(meta_v3) + data_v2_endpoints = get_endpoints(data_v2) + data_v3_endpoints = get_endpoints(data_v3) + + print( + f"Meta API - v2: {len(meta_v2_endpoints)} endpoints, v3: {len(meta_v3_endpoints)} endpoints" + ) + print( + f"Data API - v2: {len(data_v2_endpoints)} endpoints, v3: {len(data_v3_endpoints)} endpoints" + ) + + print("Analyzing differences...") + + # Simple comparison + meta_comparison = { + "removed": set(meta_v2_endpoints.keys()) - set(meta_v3_endpoints.keys()), + "new": set(meta_v3_endpoints.keys()) - set(meta_v2_endpoints.keys()), + } + + data_comparison = { + "removed": set(data_v2_endpoints.keys()) - set(data_v3_endpoints.keys()), + "new": set(data_v3_endpoints.keys()) - set(data_v2_endpoints.keys()), + } + + meta_analysis = { + "comparison": meta_comparison, + "v2_endpoints": meta_v2_endpoints, + "v3_endpoints": meta_v3_endpoints, + } + + data_analysis = { + "comparison": data_comparison, + "v2_endpoints": data_v2_endpoints, + "v3_endpoints": data_v3_endpoints, + } + + print("Generating comprehensive report...") + + report = generate_detailed_markdown(meta_analysis, data_analysis) + + # Write report + output_file = "NOCODB_API_V2_V3_COMPARISON.md" + with open(output_file, "w", encoding="utf-8") as f: + f.write(report) + + print(f"\n✓ Comprehensive report generated: {output_file}") + print("\nSummary:") + print( + f" Meta API: {len(meta_comparison['removed'])} removed, {len(meta_comparison['new'])} new" + ) + print( + f" Data API: {len(data_comparison['removed'])} removed, {len(data_comparison['new'])} new" + ) + print(f"\nReport size: {len(report)} characters") + + +if __name__ == "__main__": + main() diff --git a/scripts/migration/analyze_api_diff.py b/scripts/migration/analyze_api_diff.py new file mode 100644 index 0000000..8708714 --- /dev/null +++ b/scripts/migration/analyze_api_diff.py @@ -0,0 +1,417 @@ +#!/usr/bin/env python3 +""" +Comprehensive comparison of NocoDB API v2 vs v3 OpenAPI definitions. +Analyzes Meta and Data APIs for differences. +""" + +import json +from collections import defaultdict + + +def load_openapi(filepath: str) -> dict: + """Load OpenAPI JSON file.""" + with open(filepath, encoding="utf-8") as f: + return json.load(f) + + +def get_endpoints(openapi: dict) -> dict[str, dict]: + """Extract all endpoints from OpenAPI spec.""" + endpoints = {} + paths = openapi.get("paths", {}) + for path, methods in paths.items(): + for method, spec in methods.items(): + if method.lower() in ["get", "post", "put", "patch", "delete", "head", "options"]: + key = f"{method.upper()} {path}" + endpoints[key] = { + "path": path, + "method": method.upper(), + "spec": spec, + "summary": spec.get("summary", ""), + "description": spec.get("description", ""), + "operationId": spec.get("operationId", ""), + "tags": spec.get("tags", []), + "parameters": spec.get("parameters", []), + "requestBody": spec.get("requestBody", {}), + "responses": spec.get("responses", {}), + "security": spec.get("security", []), + } + return endpoints + + +def normalize_path(path: str) -> str: + """Normalize path for comparison by replacing parameter names.""" + import re + + # Replace {paramName} with {param} + return re.sub(r"\{[^}]+\}", "{param}", path) + + +def compare_endpoints(v2_endpoints: dict, v3_endpoints: dict) -> dict: + """Compare endpoints between v2 and v3.""" + v2_keys = set(v2_endpoints.keys()) + v3_keys = set(v3_endpoints.keys()) + + # Find exact matches, new, and removed + exact_matches = v2_keys & v3_keys + removed = v2_keys - v3_keys + new = v3_keys - v2_keys + + # Try to find renamed endpoints by comparing normalized paths + v2_normalized = {normalize_path(ep): ep for ep in v2_keys} + v3_normalized = {normalize_path(ep): ep for ep in v3_keys} + + potentially_renamed = [] + for v2_norm, v2_ep in v2_normalized.items(): + if v2_ep in removed and v2_norm in v3_normalized: + v3_ep = v3_normalized[v2_norm] + if v3_ep in new: + potentially_renamed.append((v2_ep, v3_ep)) + + return { + "exact_matches": exact_matches, + "removed": removed, + "new": new, + "potentially_renamed": potentially_renamed, + } + + +def analyze_endpoint_changes(v2_spec: dict, v3_spec: dict) -> dict: + """Analyze changes in a specific endpoint.""" + changes = {} + + # Compare parameters + v2_params = {p.get("name"): p for p in v2_spec.get("parameters", [])} + v3_params = {p.get("name"): p for p in v3_spec.get("parameters", [])} + + param_changes = { + "removed": set(v2_params.keys()) - set(v3_params.keys()), + "added": set(v3_params.keys()) - set(v2_params.keys()), + "modified": [], + } + + for param_name in set(v2_params.keys()) & set(v3_params.keys()): + v2_p = v2_params[param_name] + v3_p = v3_params[param_name] + if v2_p != v3_p: + param_changes["modified"].append({"name": param_name, "v2": v2_p, "v3": v3_p}) + + if param_changes["removed"] or param_changes["added"] or param_changes["modified"]: + changes["parameters"] = param_changes + + # Compare request body + v2_body = v2_spec.get("requestBody", {}) + v3_body = v3_spec.get("requestBody", {}) + if v2_body != v3_body: + changes["requestBody"] = { + "v2": v2_body.get("content", {}).get("application/json", {}).get("schema", {}), + "v3": v3_body.get("content", {}).get("application/json", {}).get("schema", {}), + } + + # Compare responses + v2_responses = v2_spec.get("responses", {}) + v3_responses = v3_spec.get("responses", {}) + if v2_responses != v3_responses: + changes["responses"] = {"v2": v2_responses, "v3": v3_responses} + + # Compare security + v2_security = v2_spec.get("security", []) + v3_security = v3_spec.get("security", []) + if v2_security != v3_security: + changes["security"] = {"v2": v2_security, "v3": v3_security} + + return changes + + +def categorize_endpoint(endpoint: str) -> str: + """Categorize endpoint by functionality.""" + path = endpoint.split(" ", 1)[1].lower() + + if "/tables" in path or "/table/" in path: + return "Table Operations" + elif "/records" in path or "/record/" in path or "/data/" in path: + return "Record Operations" + elif "/columns" in path or "/column/" in path or "/fields" in path: + return "Column/Field Operations" + elif "/views" in path or "/view/" in path: + return "View Operations" + elif "/links" in path or "/link/" in path or "/relations" in path: + return "Link/Relation Operations" + elif "/upload" in path or "/download" in path or "/files" in path: + return "File Operations" + elif "/hooks" in path or "/webhook" in path: + return "Webhook Operations" + elif "/meta" in path or "/schema" in path: + return "Meta Operations" + elif "/auth" in path or "/signin" in path or "/signup" in path: + return "Authentication" + elif "/bases" in path or "/base/" in path or "/projects" in path: + return "Base/Project Operations" + elif "/sources" in path or "/source/" in path: + return "Data Source Operations" + else: + return "Other" + + +def generate_markdown_report(meta_analysis: dict, data_analysis: dict) -> str: + """Generate comprehensive markdown report.""" + + md = [] + md.append("# NocoDB API v2 to v3 Comprehensive Comparison Report") + md.append("") + md.append( + f"**Generated:** {__import__('datetime').datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + md.append("") + + # Executive Summary + md.append("## Executive Summary") + md.append("") + + meta_stats = { + "removed": len(meta_analysis["comparison"]["removed"]), + "new": len(meta_analysis["comparison"]["new"]), + "renamed": len(meta_analysis["comparison"]["potentially_renamed"]), + "unchanged": len(meta_analysis["comparison"]["exact_matches"]), + } + + data_stats = { + "removed": len(data_analysis["comparison"]["removed"]), + "new": len(data_analysis["comparison"]["new"]), + "renamed": len(data_analysis["comparison"]["potentially_renamed"]), + "unchanged": len(data_analysis["comparison"]["exact_matches"]), + } + + md.append("### Meta API Changes") + md.append(f"- **Removed Endpoints:** {meta_stats['removed']}") + md.append(f"- **New Endpoints:** {meta_stats['new']}") + md.append(f"- **Potentially Renamed:** {meta_stats['renamed']}") + md.append(f"- **Unchanged:** {meta_stats['unchanged']}") + md.append("") + + md.append("### Data API Changes") + md.append(f"- **Removed Endpoints:** {data_stats['removed']}") + md.append(f"- **New Endpoints:** {data_stats['new']}") + md.append(f"- **Potentially Renamed:** {data_stats['renamed']}") + md.append(f"- **Unchanged:** {data_stats['unchanged']}") + md.append("") + + # Meta API Detailed Analysis + md.append("---") + md.append("") + md.append("## Meta API: v2 → v3 Differences") + md.append("") + + md.extend(generate_api_section(meta_analysis)) + + # Data API Detailed Analysis + md.append("---") + md.append("") + md.append("## Data API: v2 → v3 Differences") + md.append("") + + md.extend(generate_api_section(data_analysis)) + + # Breaking Changes Summary + md.append("---") + md.append("") + md.append("## Breaking Changes Summary") + md.append("") + md.append("These changes will require code modifications:") + md.append("") + + md.append("### Meta API Breaking Changes") + breaking_meta = categorize_breaking_changes(meta_analysis) + for category, changes in sorted(breaking_meta.items()): + if changes: + md.append(f"#### {category}") + for change in changes: + md.append(f"- {change}") + md.append("") + + md.append("### Data API Breaking Changes") + breaking_data = categorize_breaking_changes(data_analysis) + for category, changes in sorted(breaking_data.items()): + if changes: + md.append(f"#### {category}") + for change in changes: + md.append(f"- {change}") + md.append("") + + # Recommendations + md.append("---") + md.append("") + md.append("## Implementation Recommendations") + md.append("") + md.append("### Version Detection Strategy") + md.append("```typescript") + md.append("// Detect API version from server response") + md.append("async function detectApiVersion(baseUrl: string): Promise<'v2' | 'v3'> {") + md.append(" // Check for v3-specific endpoints or response structures") + md.append(" // Implementation depends on specific differences found") + md.append("}") + md.append("```") + md.append("") + + md.append("### Adapter Pattern") + md.append("```typescript") + md.append("interface ApiAdapter {") + md.append(" getTables(baseId: string): Promise;") + md.append(" getRecords(tableId: string, params?: QueryParams): Promise;") + md.append(" // ... other methods") + md.append("}") + md.append("") + md.append("class ApiV2Adapter implements ApiAdapter { /* ... */ }") + md.append("class ApiV3Adapter implements ApiAdapter { /* ... */ }") + md.append("```") + md.append("") + + md.append("### Migration Priority") + md.append("1. **High Priority**: Endpoints used frequently (record CRUD, table listing)") + md.append("2. **Medium Priority**: View operations, link management") + md.append("3. **Low Priority**: Advanced features, admin operations") + md.append("") + + return "\n".join(md) + + +def generate_api_section(analysis: dict) -> list[str]: + """Generate markdown section for an API.""" + md = [] + + comparison = analysis["comparison"] + v2_endpoints = analysis["v2_endpoints"] + v3_endpoints = analysis["v3_endpoints"] + + # Group by category + removed_by_category = defaultdict(list) + new_by_category = defaultdict(list) + + for endpoint in sorted(comparison["removed"]): + category = categorize_endpoint(endpoint) + removed_by_category[category].append(endpoint) + + for endpoint in sorted(comparison["new"]): + category = categorize_endpoint(endpoint) + new_by_category[category].append(endpoint) + + # Removed Endpoints + if comparison["removed"]: + md.append("### Removed Endpoints") + md.append("") + for category in sorted(removed_by_category.keys()): + md.append(f"#### {category}") + md.append("") + md.append("| Method | Path | Summary |") + md.append("|--------|------|---------|") + for endpoint in removed_by_category[category]: + method, path = endpoint.split(" ", 1) + summary = v2_endpoints[endpoint].get("summary", "N/A") + md.append(f"| `{method}` | `{path}` | {summary} |") + md.append("") + + # New Endpoints + if comparison["new"]: + md.append("### New Endpoints") + md.append("") + for category in sorted(new_by_category.keys()): + md.append(f"#### {category}") + md.append("") + md.append("| Method | Path | Summary |") + md.append("|--------|------|---------|") + for endpoint in new_by_category[category]: + method, path = endpoint.split(" ", 1) + summary = v3_endpoints[endpoint].get("summary", "N/A") + md.append(f"| `{method}` | `{path}` | {summary} |") + md.append("") + + # Potentially Renamed + if comparison["potentially_renamed"]: + md.append("### Potentially Renamed/Restructured Endpoints") + md.append("") + md.append("| v2 Endpoint | v3 Endpoint |") + md.append("|-------------|-------------|") + for v2_ep, v3_ep in sorted(comparison["potentially_renamed"]): + md.append(f"| `{v2_ep}` | `{v3_ep}` |") + md.append("") + + return md + + +def categorize_breaking_changes(analysis: dict) -> dict[str, list[str]]: + """Categorize breaking changes.""" + breaking = defaultdict(list) + + comparison = analysis["comparison"] + + # Removed endpoints are breaking + for endpoint in sorted(comparison["removed"]): + category = categorize_endpoint(endpoint) + breaking[category].append(f"**Removed:** `{endpoint}`") + + # Renamed endpoints are breaking + for v2_ep, v3_ep in comparison["potentially_renamed"]: + category = categorize_endpoint(v2_ep) + breaking[category].append(f"**Path Changed:** `{v2_ep}` → `{v3_ep}`") + + return breaking + + +def main(): + print("Loading OpenAPI specifications...") + + # Load files + meta_v2 = load_openapi("docs/nocodb-openapi-meta.json") + meta_v3 = load_openapi("docs/nocodb-openapi-meta-v3.json") + data_v2 = load_openapi("docs/nocodb-openapi-data.json") + data_v3 = load_openapi("docs/nocodb-openapi-data-v3.json") + + print("Extracting endpoints...") + + # Extract endpoints + meta_v2_endpoints = get_endpoints(meta_v2) + meta_v3_endpoints = get_endpoints(meta_v3) + data_v2_endpoints = get_endpoints(data_v2) + data_v3_endpoints = get_endpoints(data_v3) + + print( + f"Meta API - v2: {len(meta_v2_endpoints)} endpoints, v3: {len(meta_v3_endpoints)} endpoints" + ) + print( + f"Data API - v2: {len(data_v2_endpoints)} endpoints, v3: {len(data_v3_endpoints)} endpoints" + ) + + print("Comparing Meta API...") + meta_comparison = compare_endpoints(meta_v2_endpoints, meta_v3_endpoints) + + print("Comparing Data API...") + data_comparison = compare_endpoints(data_v2_endpoints, data_v3_endpoints) + + print("Generating report...") + + meta_analysis = { + "comparison": meta_comparison, + "v2_endpoints": meta_v2_endpoints, + "v3_endpoints": meta_v3_endpoints, + } + + data_analysis = { + "comparison": data_comparison, + "v2_endpoints": data_v2_endpoints, + "v3_endpoints": data_v3_endpoints, + } + + report = generate_markdown_report(meta_analysis, data_analysis) + + # Write report + output_file = "API_COMPARISON_V2_V3.md" + with open(output_file, "w", encoding="utf-8") as f: + f.write(report) + + print(f"\nReport generated: {output_file}") + print("\nQuick Stats:") + print(f"Meta API: {len(meta_comparison['removed'])} removed, {len(meta_comparison['new'])} new") + print(f"Data API: {len(data_comparison['removed'])} removed, {len(data_comparison['new'])} new") + + +if __name__ == "__main__": + main() diff --git a/scripts/migration/analyze_schemas.py b/scripts/migration/analyze_schemas.py new file mode 100644 index 0000000..ea9e068 --- /dev/null +++ b/scripts/migration/analyze_schemas.py @@ -0,0 +1,355 @@ +#!/usr/bin/env python3 +""" +Analyze request/response schemas and query parameters between v2 and v3. +""" + +import json + + +def load_openapi(filepath: str) -> dict: + """Load OpenAPI JSON file.""" + with open(filepath, encoding="utf-8") as f: + return json.load(f) + + +def extract_parameters(endpoint_spec: dict) -> dict[str, list[dict]]: + """Extract query, path, and header parameters.""" + params = {"query": [], "path": [], "header": []} + + for param in endpoint_spec.get("parameters", []): + param_type = param.get("in", "") + if param_type in params: + params[param_type].append( + { + "name": param.get("name"), + "required": param.get("required", False), + "type": param.get("schema", {}).get("type"), + "description": param.get("description", ""), + } + ) + + return params + + +def extract_request_schema(endpoint_spec: dict) -> dict: + """Extract request body schema.""" + request_body = endpoint_spec.get("requestBody", {}) + content = request_body.get("content", {}) + json_content = content.get("application/json", {}) + return json_content.get("schema", {}) + + +def extract_response_schema(endpoint_spec: dict, status_code: str = "200") -> dict: + """Extract response schema for given status code.""" + responses = endpoint_spec.get("responses", {}) + response = responses.get(status_code, {}) + content = response.get("content", {}) + json_content = content.get("application/json", {}) + return json_content.get("schema", {}) + + +def format_schema(schema: dict, indent: int = 0) -> list[str]: + """Format schema for display.""" + lines = [] + prefix = " " * indent + + if not schema: + return [f"{prefix}(No schema)"] + + schema_type = schema.get("type", "unknown") + + if schema_type == "object": + properties = schema.get("properties", {}) + required = schema.get("required", []) + + for prop_name, prop_schema in properties.items(): + req_marker = " (required)" if prop_name in required else "" + prop_type = prop_schema.get("type", "unknown") + description = prop_schema.get("description", "") + + if prop_type == "array": + items = prop_schema.get("items", {}) + items_type = items.get("type", "unknown") + lines.append(f"{prefix}- {prop_name}: {prop_type}<{items_type}>{req_marker}") + else: + lines.append(f"{prefix}- {prop_name}: {prop_type}{req_marker}") + + if description: + lines.append(f"{prefix} // {description}") + + elif schema_type == "array": + items = schema.get("items", {}) + lines.append(f"{prefix}Array of:") + lines.extend(format_schema(items, indent + 1)) + + elif "$ref" in schema: + ref = schema["$ref"] + lines.append(f"{prefix}$ref: {ref}") + + else: + lines.append(f"{prefix}Type: {schema_type}") + + return lines + + +def compare_record_operations(v2_spec: dict, v3_spec: dict) -> str: + """Compare record operation details.""" + md = [] + + md.append("## Record Operations Detailed Analysis") + md.append("") + + # Find list records endpoints + v2_list_records = None + v3_list_records = None + + for path, methods in v2_spec.get("paths", {}).items(): + if "/tables/{tableId}/records" in path and "get" in methods: + v2_list_records = methods["get"] + break + + for path, methods in v3_spec.get("paths", {}).items(): + if "/data/{baseId}/{tableId}/records" in path and "get" in methods: + v3_list_records = methods["get"] + break + + if v2_list_records and v3_list_records: + md.append("### List Records Query Parameters") + md.append("") + + v2_params = extract_parameters(v2_list_records) + v3_params = extract_parameters(v3_list_records) + + md.append("#### v2 Query Parameters") + md.append("") + if v2_params["query"]: + md.append("| Parameter | Required | Type | Description |") + md.append("|-----------|----------|------|-------------|") + for param in v2_params["query"]: + req = "Yes" if param["required"] else "No" + md.append( + f"| `{param['name']}` | {req} | {param['type']} | {param['description']} |" + ) + else: + md.append("(No query parameters documented)") + md.append("") + + md.append("#### v3 Query Parameters") + md.append("") + if v3_params["query"]: + md.append("| Parameter | Required | Type | Description |") + md.append("|-----------|----------|------|-------------|") + for param in v3_params["query"]: + req = "Yes" if param["required"] else "No" + md.append( + f"| `{param['name']}` | {req} | {param['type']} | {param['description']} |" + ) + else: + md.append("(No query parameters documented)") + md.append("") + + # Compare + v2_param_names = {p["name"] for p in v2_params["query"]} + v3_param_names = {p["name"] for p in v3_params["query"]} + + if v2_param_names != v3_param_names: + md.append("#### Parameter Changes") + md.append("") + removed = v2_param_names - v3_param_names + added = v3_param_names - v2_param_names + + if removed: + md.append(f"**Removed:** {', '.join(f'`{p}`' for p in removed)}") + if added: + md.append(f"**Added:** {', '.join(f'`{p}`' for p in added)}") + md.append("") + + # Response schema + md.append("### List Records Response Schema") + md.append("") + + v2_response = extract_response_schema(v2_list_records) + v3_response = extract_response_schema(v3_list_records) + + md.append("#### v2 Response") + md.append("```") + md.extend(format_schema(v2_response)) + md.append("```") + md.append("") + + md.append("#### v3 Response") + md.append("```") + md.extend(format_schema(v3_response)) + md.append("```") + md.append("") + + # Create record + v2_create = None + v3_create = None + + for path, methods in v2_spec.get("paths", {}).items(): + if "/tables/{tableId}/records" in path and "post" in methods: + v2_create = methods["post"] + break + + for path, methods in v3_spec.get("paths", {}).items(): + if "/data/{baseId}/{tableId}/records" in path and "post" in methods: + v3_create = methods["post"] + break + + if v2_create and v3_create: + md.append("### Create Records Request Schema") + md.append("") + + v2_request = extract_request_schema(v2_create) + v3_request = extract_request_schema(v3_create) + + md.append("#### v2 Request Body") + md.append("```") + md.extend(format_schema(v2_request)) + md.append("```") + md.append("") + + md.append("#### v3 Request Body") + md.append("```") + md.extend(format_schema(v3_request)) + md.append("```") + md.append("") + + return "\n".join(md) + + +def compare_table_operations(v2_spec: dict, v3_spec: dict) -> str: + """Compare table operation details.""" + md = [] + + md.append("## Table Operations Detailed Analysis") + md.append("") + + # Get table endpoint + v2_get_table = None + v3_get_table = None + + for path, methods in v2_spec.get("paths", {}).items(): + if path == "/api/v2/meta/tables/{tableId}" and "get" in methods: + v2_get_table = methods["get"] + break + + for path, methods in v3_spec.get("paths", {}).items(): + if "/meta/bases/{baseId}/tables/{tableId}" in path and "get" in methods: + v3_get_table = methods["get"] + break + + if v2_get_table and v3_get_table: + md.append("### Get Table Response Schema") + md.append("") + + v2_response = extract_response_schema(v2_get_table) + v3_response = extract_response_schema(v3_get_table) + + md.append("#### v2 Response") + md.append("```") + md.extend(format_schema(v2_response)) + md.append("```") + md.append("") + + md.append("#### v3 Response") + md.append("```") + md.extend(format_schema(v3_response)) + md.append("```") + md.append("") + + return "\n".join(md) + + +def generate_schema_report(v2_meta: dict, v3_meta: dict, v2_data: dict, v3_data: dict) -> str: + """Generate schema comparison report.""" + md = [] + + md.append("# NocoDB API v2 to v3 Schema & Parameter Comparison") + md.append("") + md.append( + f"**Generated:** {__import__('datetime').datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + md.append("") + + md.append("This report focuses on the detailed schema and parameter changes between v2 and v3.") + md.append("") + + # Record operations (from data API specs) + record_comparison = compare_record_operations(v2_data, v3_meta) + md.append(record_comparison) + md.append("") + + # Table operations (from meta API specs) + table_comparison = compare_table_operations(v2_meta, v3_data) + md.append(table_comparison) + md.append("") + + md.append("---") + md.append("") + md.append("## Key Schema Observations") + md.append("") + md.append("### 1. Response Envelope Structure") + md.append("") + md.append("Check if both versions use the same response envelope:") + md.append("- v2: May use `{ list: [...], pageInfo: {...} }`") + md.append("- v3: May use different structure") + md.append("") + md.append("### 2. Error Response Format") + md.append("") + md.append("Error responses may differ between versions:") + md.append("```typescript") + md.append("// v2 error format (typical)") + md.append("{") + md.append(' "msg": "Error message",') + md.append(' "error": "ERROR_CODE"') + md.append("}") + md.append("") + md.append("// v3 error format (may differ)") + md.append("{") + md.append(' "message": "Error message",') + md.append(' "statusCode": 400,') + md.append(' "error": "Bad Request"') + md.append("}") + md.append("```") + md.append("") + md.append("### 3. Pagination") + md.append("") + md.append("Both versions should be checked for:") + md.append("- Offset/limit based pagination") + md.append("- Cursor-based pagination") + md.append("- Page info structure") + md.append("") + md.append("### 4. Field Names") + md.append("") + md.append("Notable terminology changes:") + md.append("- `columns` → `fields`") + md.append("- Check if `Id` vs `id` (capitalization)") + md.append("- Check timestamp field names") + md.append("") + + return "\n".join(md) + + +def main(): + print("Loading OpenAPI specifications...") + + v2_meta = load_openapi("docs/nocodb-openapi-meta.json") + v3_meta = load_openapi("docs/nocodb-openapi-meta-v3.json") + v2_data = load_openapi("docs/nocodb-openapi-data.json") + v3_data = load_openapi("docs/nocodb-openapi-data-v3.json") + + print("Analyzing schemas...") + + report = generate_schema_report(v2_meta, v3_meta, v2_data, v3_data) + + output_file = "NOCODB_API_SCHEMA_COMPARISON.md" + with open(output_file, "w", encoding="utf-8") as f: + f.write(report) + + print(f"Schema comparison report generated: {output_file}") + + +if __name__ == "__main__": + main() From 9356b084464d75fcd34d1ff0d73dacbf8ac9095f Mon Sep 17 00:00:00 2001 From: Karl Bauer Date: Fri, 10 Oct 2025 13:07:24 +0200 Subject: [PATCH 4/7] feat: Enhance Client to support both API v2 and v3 - Updated methods in NocoDBMetaClient to dynamically construct API endpoints based on the API version. - Added support for base_id in various methods to accommodate API v3 requirements. - Modified test cases to ensure compatibility with the new API versioning and endpoint structure. - Adjusted mock setups in tests to reflect changes in API version handling. --- docs/API_VERSION_GUIDE.md | 312 +++++++++++ examples/api_version_example.py | 270 +++++++++ examples/meta_api_version_example.py | 375 +++++++++++++ src/nocodb_simple_client/__init__.py | 7 + src/nocodb_simple_client/api_version.py | 635 ++++++++++++++++++++++ src/nocodb_simple_client/base_resolver.py | 164 ++++++ src/nocodb_simple_client/client.py | 233 +++++++- src/nocodb_simple_client/meta_client.py | 364 +++++++++++-- tests/test_api_version.py | 407 ++++++++++++++ tests/test_client.py | 2 +- tests/test_meta_client.py | 36 +- 11 files changed, 2728 insertions(+), 77 deletions(-) create mode 100644 docs/API_VERSION_GUIDE.md create mode 100644 examples/api_version_example.py create mode 100644 examples/meta_api_version_example.py create mode 100644 src/nocodb_simple_client/api_version.py create mode 100644 src/nocodb_simple_client/base_resolver.py create mode 100644 tests/test_api_version.py diff --git a/docs/API_VERSION_GUIDE.md b/docs/API_VERSION_GUIDE.md new file mode 100644 index 0000000..08f81cf --- /dev/null +++ b/docs/API_VERSION_GUIDE.md @@ -0,0 +1,312 @@ +# NocoDB API Version Guide (v2 vs v3) + +## Overview + +The NocoDB Simple Client now supports both **API v2** (default) and **API v3** with seamless switching between versions. + +## Quick Start + +### Using API v2 (Default) + +```python +from nocodb_simple_client import NocoDBClient + +# v2 is the default - no changes needed to existing code +client = NocoDBClient( + base_url="https://app.nocodb.com", + db_auth_token="your-api-token" +) + +# All existing code works without modification +records = client.get_records("table_id", limit=10) +``` + +### Using API v3 + +```python +from nocodb_simple_client import NocoDBClient + +# Specify v3 and provide base_id +client = NocoDBClient( + base_url="https://app.nocodb.com", + db_auth_token="your-api-token", + api_version="v3", + base_id="your_base_id" # Required for v3 +) + +# Same API, same syntax! +records = client.get_records("table_id", limit=10) +``` + +## Key Differences Between v2 and v3 + +### 1. **Base ID Requirement** + +| Aspect | v2 | v3 | +|--------|----|----| +| **Base ID** | Optional (implicit) | **Required** in all operations | +| **How to provide** | Not needed | Set in constructor or per-method | + +**v3 Examples:** + +```python +# Option 1: Set default base_id in constructor (recommended) +client = NocoDBClient( + base_url="...", + db_auth_token="...", + api_version="v3", + base_id="base_abc123" # Used for all operations +) + +records = client.get_records("table_xyz") # base_id used automatically + +# Option 2: Provide base_id per method call +client = NocoDBClient( + base_url="...", + db_auth_token="...", + api_version="v3" +) + +records = client.get_records("table_xyz", base_id="base_abc123") +``` + +### 2. **Pagination** + +| Aspect | v2 | v3 | +|--------|----|----| +| **Parameters** | `offset` and `limit` | `page` and `pageSize` | +| **Conversion** | Automatic | Handled internally | + +**You don't need to change your code!** The client automatically converts: + +```python +# Your code (same for both v2 and v3): +records = client.get_records("table_id", limit=25) + +# v2 internally uses: offset=0, limit=25 +# v3 internally converts to: page=1, pageSize=25 +``` + +### 3. **Sort Format** + +| Aspect | v2 | v3 | +|--------|----|----| +| **Format** | String: `"field1,-field2"` | JSON array | +| **Conversion** | Automatic | Handled internally | + +**You don't need to change your code!** + +```python +# Your code (same for both v2 and v3): +records = client.get_records( + "table_id", + sort="Name,-CreatedAt" # Name ASC, CreatedAt DESC +) + +# v2 uses as-is: "Name,-CreatedAt" +# v3 converts to: [{"field": "Name", "direction": "asc"}, +# {"field": "CreatedAt", "direction": "desc"}] +``` + +### 4. **Query Operators** + +| Operator | v2 | v3 | +|----------|----|----| +| Not Equal | `ne` | `neq` | +| Others | Same | Same | + +**Conversion handled automatically!** + +## API Methods Support + +All methods support both v2 and v3: + +✅ **Record Operations** +- `get_records()` +- `get_record()` +- `insert_record()` +- `update_record()` +- `delete_record()` +- `count_records()` + +✅ **Bulk Operations** +- `bulk_insert_records()` +- `bulk_update_records()` +- `bulk_delete_records()` + +✅ **File Operations** +- `attach_file_to_record()` +- `attach_files_to_record()` +- `delete_file_from_record()` +- `download_file_from_record()` +- `download_files_from_record()` + +## Migration Guide + +### Step 1: Test v3 in Parallel + +```python +# Keep existing v2 client +client_v2 = NocoDBClient( + base_url="...", + db_auth_token="...", + api_version="v2" +) + +# Create v3 client for testing +client_v3 = NocoDBClient( + base_url="...", + db_auth_token="...", + api_version="v3", + base_id="your_base_id" +) + +# Compare results +records_v2 = client_v2.get_records("table_id") +records_v3 = client_v3.get_records("table_id") +``` + +### Step 2: Switch When Ready + +```python +# Simply change these two lines: +client = NocoDBClient( + base_url="...", + db_auth_token="...", + api_version="v3", # Changed from "v2" + base_id="your_base_id" # Added this line +) + +# All other code remains the same! +``` + +## Advanced Features + +### Base ID Resolver + +For v3, the client includes automatic base ID resolution: + +```python +client = NocoDBClient( + base_url="...", + db_auth_token="...", + api_version="v3", + base_id="default_base" +) + +# Can manually set base_id mappings +client._base_resolver.set_base_id("table_123", "base_abc") + +# Clear cache if needed +client._base_resolver.clear_cache() +``` + +### Query Parameter Adapter + +Advanced users can access the parameter adapter directly: + +```python +from nocodb_simple_client import QueryParamAdapter + +adapter = QueryParamAdapter() + +# Convert pagination +v3_params = adapter.convert_pagination_to_v3({"offset": 50, "limit": 25}) +# Returns: {"page": 3, "pageSize": 25} + +# Convert sort format +v3_sort = adapter.convert_sort_to_v3("name,-age") +# Returns: [{"field": "name", "direction": "asc"}, +# {"field": "age", "direction": "desc"}] +``` + +### Path Builder + +Build API paths programmatically: + +```python +from nocodb_simple_client import PathBuilder, APIVersion + +# Create builder for v3 +builder = PathBuilder(APIVersion.V3) + +# Build record endpoint +path = builder.records_list("table_123", "base_abc") +# Returns: "api/v3/data/base_abc/table_123/records" +``` + +## Troubleshooting + +### Error: "base_id is required for API v3" + +**Solution:** Provide `base_id` either in the constructor or method call: + +```python +# Option 1: In constructor +client = NocoDBClient(..., api_version="v3", base_id="your_base_id") + +# Option 2: Per method +records = client.get_records("table_id", base_id="your_base_id") +``` + +### Pagination Not Working as Expected + +The client automatically converts pagination parameters. If you experience issues, ensure you're using the standard `limit` parameter: + +```python +# ✅ Correct +records = client.get_records("table_id", limit=25) + +# ❌ Don't manually specify page/pageSize for v3 +records = client.get_records("table_id", page=1, pageSize=25) # Wrong! +``` + +## Performance Considerations + +### Base ID Caching + +When using v3, the client caches base_id lookups: + +```python +# First call: Makes API request to resolve base_id +client.get_records("table_123") + +# Subsequent calls: Uses cached base_id (faster) +client.get_records("table_123") + +# Clear cache if base_id changes +client._base_resolver.clear_cache("table_123") +``` + +### Batch Operations + +Both v2 and v3 support bulk operations for better performance: + +```python +# Insert multiple records at once +records = [ + {"Name": "John", "Age": 30}, + {"Name": "Jane", "Age": 25}, +] + +record_ids = client.bulk_insert_records("table_id", records) +``` + +## Best Practices + +1. **Use v3 for new projects** - v3 is the future of NocoDB API +2. **Set base_id in constructor** - Cleaner code and better performance +3. **Test thoroughly** - Use parallel testing before migrating production code +4. **Don't mix parameters** - Let the client handle conversions automatically +5. **Cache base_id mappings** - Pre-populate resolver for known tables + +## Examples + +See complete examples in: +- [`examples/api_version_example.py`](../examples/api_version_example.py) + +## Resources + +- [API v2 to v3 Migration Guide](../API_V2_V3_MIGRATION_GUIDE.md) +- [Detailed API Comparison](../NOCODB_API_V2_V3_COMPARISON.md) +- [Schema Differences](../NOCODB_API_SCHEMA_COMPARISON.md) diff --git a/examples/api_version_example.py b/examples/api_version_example.py new file mode 100644 index 0000000..1aea3ae --- /dev/null +++ b/examples/api_version_example.py @@ -0,0 +1,270 @@ +""" +Example demonstrating NocoDB API v2 and v3 usage. + +This example shows how to use the NocoDB client with both API versions. +""" + +from nocodb_simple_client import NocoDBClient + +# ============================================================================ +# API v2 Example (Default) +# ============================================================================ + +print("=" * 60) +print("API v2 Example (Default)") +print("=" * 60) + +# Create client with v2 API (default) +client_v2 = NocoDBClient( + base_url="https://app.nocodb.com", + db_auth_token="your-api-token-here", + # api_version="v2" # This is the default, can be omitted +) + +print(f"Client API Version: {client_v2.api_version}") + +# Get records from a table (v2 style) +# No base_id required for v2 +records = client_v2.get_records( + table_id="tbl_abc123", + limit=10, + where="(Status,eq,Active)", + sort="-CreatedAt", # v2 uses string format: "-field" for DESC +) + +print(f"Found {len(records)} records") + +# Insert a record (v2 style) +new_record = { + "Name": "John Doe", + "Email": "john@example.com", + "Status": "Active", +} + +record_id = client_v2.insert_record( + table_id="tbl_abc123", + record=new_record, +) + +print(f"Inserted record with ID: {record_id}") + +# Update a record (v2 style) +update_data = {"Status": "Inactive"} + +client_v2.update_record( + table_id="tbl_abc123", + record=update_data, + record_id=record_id, +) + +print(f"Updated record {record_id}") + +# ============================================================================ +# API v3 Example +# ============================================================================ + +print("\n" + "=" * 60) +print("API v3 Example") +print("=" * 60) + +# Create client with v3 API +# Option 1: Provide base_id in constructor (recommended) +client_v3 = NocoDBClient( + base_url="https://app.nocodb.com", + db_auth_token="your-api-token-here", + api_version="v3", + base_id="base_xyz789", # Default base_id for all operations +) + +print(f"Client API Version: {client_v3.api_version}") +print(f"Default Base ID: {client_v3.base_id}") + +# Get records from a table (v3 style) +# base_id is automatically used from client's default +records_v3 = client_v3.get_records( + table_id="tbl_abc123", + limit=10, + where="(Status,eq,Active)", + sort="-CreatedAt", # Still uses v2 string format, converted internally +) + +print(f"Found {len(records_v3)} records") + +# You can also override the base_id for specific calls +records_v3_alt = client_v3.get_records( + table_id="tbl_def456", + base_id="base_different123", # Override default base_id + limit=5, +) + +print(f"Found {len(records_v3_alt)} records from different base") + +# Insert a record (v3 style) +new_record_v3 = { + "Name": "Jane Smith", + "Email": "jane@example.com", + "Status": "Active", +} + +record_id_v3 = client_v3.insert_record( + table_id="tbl_abc123", + record=new_record_v3, + # base_id is automatically used from client's default +) + +print(f"Inserted record with ID: {record_id_v3}") + +# ============================================================================ +# API v3 with Manual Base ID Resolution +# ============================================================================ + +print("\n" + "=" * 60) +print("API v3 with Manual Base ID (No Auto-Resolution)") +print("=" * 60) + +# Option 2: No default base_id, provide it with each call +client_v3_manual = NocoDBClient( + base_url="https://app.nocodb.com", + db_auth_token="your-api-token-here", + api_version="v3", + # No base_id provided - must specify for each call +) + +# This requires base_id in every method call +try: + records_manual = client_v3_manual.get_records( + table_id="tbl_abc123", + base_id="base_xyz789", # Required! + limit=10, + ) + print(f"Found {len(records_manual)} records") +except ValueError as e: + print(f"Error: {e}") + +# ============================================================================ +# Pagination Differences (Handled Automatically) +# ============================================================================ + +print("\n" + "=" * 60) +print("Pagination Handling (Automatic Conversion)") +print("=" * 60) + +# v2 client - uses offset/limit internally +print("\\nv2 Pagination:") +records_v2_page = client_v2.get_records( + table_id="tbl_abc123", + limit=25, + # Internally uses: offset=0, limit=25 +) +print(f"Retrieved {len(records_v2_page)} records") + +# v3 client - automatically converts to page/pageSize +print("\\nv3 Pagination:") +records_v3_page = client_v3.get_records( + table_id="tbl_abc123", + limit=25, + # Internally converts to: page=1, pageSize=25 +) +print(f"Retrieved {len(records_v3_page)} records") + +# ============================================================================ +# Sort Format Differences (Handled Automatically) +# ============================================================================ + +print("\n" + "=" * 60) +print("Sort Format Handling (Automatic Conversion)") +print("=" * 60) + +# v2 uses string format +records_v2_sorted = client_v2.get_records( + table_id="tbl_abc123", + sort="Name,-CreatedAt", # Sort by Name ASC, then CreatedAt DESC + limit=10, +) +print(f"v2 sorted {len(records_v2_sorted)} records") + +# v3 automatically converts to JSON format +records_v3_sorted = client_v3.get_records( + table_id="tbl_abc123", + sort="Name,-CreatedAt", # Same syntax! Converted internally to JSON + limit=10, +) +print(f"v3 sorted {len(records_v3_sorted)} records") + +# ============================================================================ +# File Operations (Both Versions) +# ============================================================================ + +print("\n" + "=" * 60) +print("File Operations") +print("=" * 60) + +# v2 file upload +print("\\nv2 File Upload:") +record_id_v2 = client_v2.attach_file_to_record( + table_id="tbl_abc123", + record_id=123, + field_name="Attachments", + file_path="/path/to/document.pdf", +) +print(f"Attached file to record {record_id_v2}") + +# v3 file upload (with base_id) +print("\\nv3 File Upload:") +record_id_v3_file = client_v3.attach_file_to_record( + table_id="tbl_abc123", + record_id=123, + field_name="Attachments", + file_path="/path/to/document.pdf", + # base_id automatically used from client's default +) +print(f"Attached file to record {record_id_v3_file}") + +# ============================================================================ +# Migration Path: Gradual Transition from v2 to v3 +# ============================================================================ + +print("\n" + "=" * 60) +print("Migration Path: v2 to v3 Transition") +print("=" * 60) + +# Step 1: Start with v2 (current code) +legacy_client = NocoDBClient( + base_url="https://app.nocodb.com", + db_auth_token="your-api-token-here", + api_version="v2", # Explicitly set to v2 +) + +# Step 2: Test v3 in parallel +test_client_v3 = NocoDBClient( + base_url="https://app.nocodb.com", + db_auth_token="your-api-token-here", + api_version="v3", + base_id="base_xyz789", +) + +# Step 3: Compare results +records_legacy = legacy_client.get_records("tbl_abc123", limit=5) +records_new = test_client_v3.get_records("tbl_abc123", limit=5) + +print(f"v2 returned {len(records_legacy)} records") +print(f"v3 returned {len(records_new)} records") + +# Step 4: Once validated, switch to v3 by changing api_version parameter + +print("\n" + "=" * 60) +print("All examples completed!") +print("=" * 60) + +# ============================================================================ +# Best Practices +# ============================================================================ + +print("\\n\\nBEST PRACTICES:") +print("-" * 60) +print("1. For v2: Use default settings (api_version='v2', no base_id)") +print("2. For v3: Set api_version='v3' and provide base_id in constructor") +print("3. Migration: Test v3 in parallel before switching production code") +print("4. Use same query syntax - conversion happens automatically") +print("5. base_id can be set per-client (recommended) or per-method call") +print("-" * 60) diff --git a/examples/meta_api_version_example.py b/examples/meta_api_version_example.py new file mode 100644 index 0000000..3c000b1 --- /dev/null +++ b/examples/meta_api_version_example.py @@ -0,0 +1,375 @@ +""" +Example demonstrating NocoDB Meta API v2 and v3 usage. + +This example shows how to use the Meta API client with both versions +for managing database structure (tables, columns, views, webhooks). +""" + +from nocodb_simple_client import NocoDBMetaClient + +# ============================================================================ +# Meta API v2 Example (Default) +# ============================================================================ + +print("=" * 60) +print("Meta API v2 Example (Default)") +print("=" * 60) + +# Create Meta API client with v2 (default) +meta_v2 = NocoDBMetaClient( + base_url="https://app.nocodb.com", + db_auth_token="your-api-token-here", + # api_version="v2" # Default, can be omitted +) + +print(f"Client API Version: {meta_v2.api_version}") + +# List all bases +bases = meta_v2.list_bases() +print(f"Found {len(bases)} bases") + +# List tables in a base +tables = meta_v2.list_tables(base_id="base_abc123") +print(f"Found {len(tables)} tables") + +# Get table metadata (v2 - no base_id required) +table_info = meta_v2.get_table_info(table_id="tbl_xyz789") +print(f"Table: {table_info.get('title', 'Unknown')}") + +# List columns in a table (v2 - no base_id required) +columns = meta_v2.list_columns(table_id="tbl_xyz789") +print(f"Table has {len(columns)} columns") + +# Create a new column +new_column = { + "title": "Status", + "uidt": "SingleSelect", # UI Data Type + "dtxp": "Active,Inactive,Pending", # Options +} + +column = meta_v2.create_column( + table_id="tbl_xyz789", + column_data=new_column, +) +print(f"Created column: {column.get('title', 'Unknown')}") + +# List views +views = meta_v2.list_views(table_id="tbl_xyz789") +print(f"Table has {len(views)} views") + +# List webhooks +webhooks = meta_v2.list_webhooks(table_id="tbl_xyz789") +print(f"Table has {len(webhooks)} webhooks") + +# ============================================================================ +# Meta API v3 Example +# ============================================================================ + +print("\n" + "=" * 60) +print("Meta API v3 Example") +print("=" * 60) + +# Create Meta API client with v3 +meta_v3 = NocoDBMetaClient( + base_url="https://app.nocodb.com", + db_auth_token="your-api-token-here", + api_version="v3", + base_id="base_abc123", # Default base_id for all operations +) + +print(f"Client API Version: {meta_v3.api_version}") +print(f"Default Base ID: {meta_v3.base_id}") + +# ============================================================================ +# Workspace & Base Operations (Same for v2 and v3) +# ============================================================================ + +print("\n" + "=" * 60) +print("Workspace & Base Operations (v2/v3 Compatible)") +print("=" * 60) + +# List workspaces (same endpoint for v2 and v3) +workspaces = meta_v3.list_workspaces() +print(f"Found {len(workspaces)} workspaces") + +# Get workspace details +if workspaces: + workspace = meta_v3.get_workspace(workspaces[0]["id"]) + print(f"Workspace: {workspace.get('title', 'Unknown')}") + +# List all bases +bases_v3 = meta_v3.list_bases() +print(f"Found {len(bases_v3)} bases") + +# Get base details +base = meta_v3.get_base(base_id="base_abc123") +print(f"Base: {base.get('title', 'Unknown')}") + +# ============================================================================ +# Table Operations (v3 with base_id) +# ============================================================================ + +print("\n" + "=" * 60) +print("Table Operations (v3)") +print("=" * 60) + +# List tables (base_id already known) +tables_v3 = meta_v3.list_tables(base_id="base_abc123") +print(f"Found {len(tables_v3)} tables") + +# Get table info (v3 - uses default base_id) +table_info_v3 = meta_v3.get_table_info(table_id="tbl_xyz789") +print(f"Table: {table_info_v3.get('title', 'Unknown')}") + +# Or override with specific base_id +table_info_alt = meta_v3.get_table_info( + table_id="tbl_different", + base_id="base_different123", +) +print(f"Alternate table: {table_info_alt.get('title', 'Unknown')}") + +# Create a new table +table_data = { + "title": "Customers", + "description": "Customer database", + "columns": [ + { + "title": "Name", + "uidt": "SingleLineText", + "pv": True, # Primary value + }, + { + "title": "Email", + "uidt": "Email", + }, + { + "title": "Phone", + "uidt": "PhoneNumber", + }, + { + "title": "Created At", + "uidt": "DateTime", + "dtxp": "YYYY-MM-DD HH:mm:ss", + }, + ], +} + +new_table = meta_v3.create_table( + base_id="base_abc123", + table_data=table_data, +) +print(f"Created table: {new_table.get('title', 'Unknown')}") + +# Update table metadata +updated_table = meta_v3.update_table( + table_id=new_table["id"], + table_data={"description": "Updated customer database"}, + # base_id automatically used from client's default +) +print("Updated table description") + +# ============================================================================ +# Column Operations (v3 - columns are called "fields") +# ============================================================================ + +print("\n" + "=" * 60) +print("Column/Field Operations (v3)") +print("=" * 60) + +# List columns (v3 - automatically uses default base_id) +columns_v3 = meta_v3.list_columns(table_id="tbl_xyz789") +print(f"Table has {len(columns_v3)} columns/fields") + +# Create a new column/field +column_data = { + "title": "Priority", + "uidt": "SingleSelect", + "dtxp": "Low,Medium,High,Urgent", # Options + "dtxs": "Medium", # Default value +} + +new_column_v3 = meta_v3.create_column( + table_id="tbl_xyz789", + column_data=column_data, + # base_id automatically used +) +print(f"Created field: {new_column_v3.get('title', 'Unknown')}") + +# Update column (v3 requires base_id for column_id operations) +updated_column = meta_v3.update_column( + column_id=new_column_v3["id"], + column_data={"title": "Task Priority"}, + base_id="base_abc123", # Required for v3 when using column_id +) +print("Updated field title") + +# ============================================================================ +# View Operations (v3) +# ============================================================================ + +print("\n" + "=" * 60) +print("View Operations (v3)") +print("=" * 60) + +# List views for a table +views_v3 = meta_v3.list_views(table_id="tbl_xyz789") +print(f"Table has {len(views_v3)} views") + +# Create a new view +view_data = { + "title": "Active Customers", + "type": "Grid", + "show_system_fields": False, + "lock_type": "collaborative", +} + +new_view = meta_v3.create_view( + table_id="tbl_xyz789", + view_data=view_data, + # base_id automatically used +) +print(f"Created view: {new_view.get('title', 'Unknown')}") + +# Update view (requires base_id for view_id operations) +updated_view = meta_v3.update_view( + view_id=new_view["id"], + view_data={"title": "All Active Customers"}, + base_id="base_abc123", # Required for v3 +) +print("Updated view title") + +# Get view details +view_details = meta_v3.get_view( + view_id=new_view["id"], + base_id="base_abc123", +) +print(f"View type: {view_details.get('type', 'Unknown')}") + +# ============================================================================ +# Webhook Operations (v3) +# ============================================================================ + +print("\n" + "=" * 60) +print("Webhook Operations (v3)") +print("=" * 60) + +# List webhooks for a table +webhooks_v3 = meta_v3.list_webhooks(table_id="tbl_xyz789") +print(f"Table has {len(webhooks_v3)} webhooks") + +# Create a webhook +webhook_data = { + "title": "New Customer Notification", + "event": "after", + "operation": "insert", + "notification": { + "type": "URL", + "payload": { + "method": "POST", + "url": "https://hooks.example.com/customer-webhook", + "body": '{"customer": "{{Name}}", "email": "{{Email}}"}', + "headers": [{"name": "Content-Type", "value": "application/json"}], + }, + }, + "condition": [], # No conditions, trigger on all inserts + "active": True, +} + +new_webhook = meta_v3.create_webhook( + table_id="tbl_xyz789", + webhook_data=webhook_data, + # base_id automatically used +) +print(f"Created webhook: {new_webhook.get('title', 'Unknown')}") + +# Update webhook +updated_webhook = meta_v3.update_webhook( + hook_id=new_webhook["id"], + webhook_data={"active": False}, + base_id="base_abc123", # Required for v3 +) +print("Disabled webhook") + +# Test webhook +test_result = meta_v3.test_webhook( + hook_id=new_webhook["id"], + base_id="base_abc123", +) +print(f"Webhook test result: {test_result.get('status', 'Unknown')}") + +# ============================================================================ +# Migration: Comparing v2 and v3 Side-by-Side +# ============================================================================ + +print("\n" + "=" * 60) +print("Migration Example: v2 vs v3 Comparison") +print("=" * 60) + +# Same operation in v2 and v3 +print("\nGetting table info:") + +# v2 - no base_id needed +table_v2 = meta_v2.get_table_info(table_id="tbl_xyz789") +print(f"v2: {table_v2.get('title', 'Unknown')}") + +# v3 - uses default base_id from client +table_v3 = meta_v3.get_table_info(table_id="tbl_xyz789") +print(f"v3: {table_v3.get('title', 'Unknown')}") + +print("\nListing columns:") + +# v2 - no base_id needed +cols_v2 = meta_v2.list_columns(table_id="tbl_xyz789") +print(f"v2: {len(cols_v2)} columns") + +# v3 - uses default base_id, columns are called "fields" +cols_v3 = meta_v3.list_columns(table_id="tbl_xyz789") +print(f"v3: {len(cols_v3)} fields") + +# ============================================================================ +# Advanced: Using Multiple Bases in v3 +# ============================================================================ + +print("\n" + "=" * 60) +print("Advanced: Working with Multiple Bases (v3)") +print("=" * 60) + +# Client configured with default base +meta_multi = NocoDBMetaClient( + base_url="https://app.nocodb.com", + db_auth_token="your-api-token-here", + api_version="v3", + base_id="base_primary", +) + +# Use default base +tables_primary = meta_multi.list_tables(base_id="base_primary") +print(f"Primary base: {len(tables_primary)} tables") + +# Override for different base +tables_secondary = meta_multi.list_tables(base_id="base_secondary") +print(f"Secondary base: {len(tables_secondary)} tables") + +# Table operations with explicit base_id override +table_from_other_base = meta_multi.get_table_info( + table_id="tbl_from_secondary", + base_id="base_secondary", # Override default +) +print(f"Table from secondary base: {table_from_other_base.get('title', 'Unknown')}") + +# ============================================================================ +# Best Practices Summary +# ============================================================================ + +print("\n" + "=" * 60) +print("BEST PRACTICES") +print("=" * 60) +print("1. v2: Simple, no base_id needed for table operations") +print("2. v3: Set base_id in constructor for cleaner code") +print("3. v3: For column/view/webhook operations by ID, always provide base_id") +print("4. Migration: Test v3 in parallel before switching") +print("5. Columns → Fields: v3 terminology (PathBuilder handles this)") +print("6. Workspace/Base endpoints: Same for v2 and v3") +print("=" * 60) + +print("\n✅ All Meta API examples completed!") diff --git a/src/nocodb_simple_client/__init__.py b/src/nocodb_simple_client/__init__.py index 7a59dd3..e9d87bb 100644 --- a/src/nocodb_simple_client/__init__.py +++ b/src/nocodb_simple_client/__init__.py @@ -26,6 +26,8 @@ # Async support (optional) from typing import TYPE_CHECKING +from .api_version import APIVersion, PathBuilder, QueryParamAdapter +from .base_resolver import BaseIdResolver from .cache import CacheManager from .client import NocoDBClient from .columns import NocoDBColumns, TableColumns @@ -88,6 +90,11 @@ def __init__(self, *args, **kwargs): # type: ignore[misc] "NocoDBClient", "NocoDBTable", "NocoDBMetaClient", + # API Version support + "APIVersion", + "PathBuilder", + "QueryParamAdapter", + "BaseIdResolver", # Exceptions "NocoDBException", "RecordNotFoundException", diff --git a/src/nocodb_simple_client/api_version.py b/src/nocodb_simple_client/api_version.py new file mode 100644 index 0000000..6d26d48 --- /dev/null +++ b/src/nocodb_simple_client/api_version.py @@ -0,0 +1,635 @@ +"""NocoDB API version support and adapters. + +MIT License + +Copyright (c) BAUER GROUP + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from enum import Enum +from typing import Any + + +class APIVersion(str, Enum): + """NocoDB API version.""" + + V2 = "v2" + V3 = "v3" + + def __str__(self) -> str: + return self.value + + +class QueryParamAdapter: + """Adapter for converting query parameters between API versions.""" + + @staticmethod + def convert_pagination_to_v3(params: dict[str, Any]) -> dict[str, Any]: + """Convert v2 offset/limit pagination to v3 page/pageSize. + + Args: + params: Query parameters dict (may be modified in place) + + Returns: + Modified parameters dict with v3 pagination + + Example: + >>> params = {"offset": 50, "limit": 25} + >>> QueryParamAdapter.convert_pagination_to_v3(params) + {'page': 3, 'pageSize': 25} + """ + result = params.copy() + + # Convert offset/limit to page/pageSize + if "offset" in result or "limit" in result: + offset = result.pop("offset", 0) + limit = result.pop("limit", 25) + + # Calculate page number (1-indexed) + page = (offset // limit) + 1 if limit > 0 else 1 + result["page"] = page + result["pageSize"] = limit + + return result + + @staticmethod + def convert_pagination_to_v2(params: dict[str, Any]) -> dict[str, Any]: + """Convert v3 page/pageSize pagination to v2 offset/limit. + + Args: + params: Query parameters dict (may be modified in place) + + Returns: + Modified parameters dict with v2 pagination + + Example: + >>> params = {"page": 3, "pageSize": 25} + >>> QueryParamAdapter.convert_pagination_to_v2(params) + {'offset': 50, 'limit': 25} + """ + result = params.copy() + + # Convert page/pageSize to offset/limit + if "page" in result or "pageSize" in result: + page = result.pop("page", 1) + page_size = result.pop("pageSize", 25) + + # Calculate offset (0-indexed) + offset = (page - 1) * page_size if page > 0 else 0 + result["offset"] = offset + result["limit"] = page_size + + return result + + @staticmethod + def convert_sort_to_v3(sort_str: str | None) -> list[dict[str, str]] | None: + """Convert v2 sort string to v3 JSON array format. + + Args: + sort_str: v2 sort string (e.g., "field1,-field2") + + Returns: + v3 sort array or None + + Example: + >>> QueryParamAdapter.convert_sort_to_v3("name,-age") + [{'field': 'name', 'direction': 'asc'}, {'field': 'age', 'direction': 'desc'}] + """ + if not sort_str: + return None + + sorts = [] + for field in sort_str.split(","): + field = field.strip() + if field.startswith("-"): + sorts.append({"field": field[1:], "direction": "desc"}) + else: + sorts.append({"field": field, "direction": "asc"}) + + return sorts if sorts else None + + @staticmethod + def convert_sort_to_v2(sort_list: list[dict[str, str]] | None) -> str | None: + """Convert v3 sort JSON array to v2 string format. + + Args: + sort_list: v3 sort array + + Returns: + v2 sort string or None + + Example: + >>> sort = [{'field': 'name', 'direction': 'asc'}, {'field': 'age', 'direction': 'desc'}] + >>> QueryParamAdapter.convert_sort_to_v2(sort) + 'name,-age' + """ + if not sort_list: + return None + + fields = [] + for sort_item in sort_list: + field = sort_item["field"] + direction = sort_item.get("direction", "asc") + if direction == "desc": + fields.append(f"-{field}") + else: + fields.append(field) + + return ",".join(fields) if fields else None + + @staticmethod + def convert_where_operators_to_v3(where: dict[str, Any] | None) -> dict[str, Any] | None: + """Convert v2 where operators to v3 format. + + Changes 'ne' to 'neq' operator. + + Args: + where: Where clause dict + + Returns: + Modified where clause for v3 + """ + if not where: + return where + + # Deep copy to avoid modifying original + import json + + result = json.loads(json.dumps(where)) + + def replace_ne(obj: Any) -> Any: + if isinstance(obj, dict): + # Replace 'ne' with 'neq' + if "ne" in obj: + obj["neq"] = obj.pop("ne") + # Recursively process nested dicts + for value in obj.values(): + replace_ne(value) + elif isinstance(obj, list): + for item in obj: + replace_ne(item) + return obj + + return replace_ne(result) + + @staticmethod + def convert_where_operators_to_v2(where: dict[str, Any] | None) -> dict[str, Any] | None: + """Convert v3 where operators to v2 format. + + Changes 'neq' to 'ne' operator. + + Args: + where: Where clause dict + + Returns: + Modified where clause for v2 + """ + if not where: + return where + + # Deep copy to avoid modifying original + import json + + result = json.loads(json.dumps(where)) + + def replace_neq(obj: Any) -> Any: + if isinstance(obj, dict): + # Replace 'neq' with 'ne' + if "neq" in obj: + obj["ne"] = obj.pop("neq") + # Recursively process nested dicts + for value in obj.values(): + replace_neq(value) + elif isinstance(obj, list): + for item in obj: + replace_neq(item) + return obj + + return replace_neq(result) + + +class PathBuilder: + """Builder for constructing API endpoint paths for different versions.""" + + def __init__(self, api_version: APIVersion): + """Initialize path builder. + + Args: + api_version: The API version to use + """ + self.api_version = api_version + + def records_list(self, table_id: str, base_id: str | None = None) -> str: + """Build path for listing records. + + Args: + table_id: Table ID + base_id: Base ID (required for v3) + + Returns: + API endpoint path + """ + if self.api_version == APIVersion.V2: + return f"api/v2/tables/{table_id}/records" + else: # V3 + if not base_id: + raise ValueError("base_id is required for API v3") + return f"api/v3/data/{base_id}/{table_id}/records" + + def records_get(self, table_id: str, record_id: str, base_id: str | None = None) -> str: + """Build path for getting a single record. + + Args: + table_id: Table ID + record_id: Record ID + base_id: Base ID (required for v3) + + Returns: + API endpoint path + """ + if self.api_version == APIVersion.V2: + return f"api/v2/tables/{table_id}/records/{record_id}" + else: # V3 + if not base_id: + raise ValueError("base_id is required for API v3") + return f"api/v3/data/{base_id}/{table_id}/records/{record_id}" + + def records_create(self, table_id: str, base_id: str | None = None) -> str: + """Build path for creating records. + + Args: + table_id: Table ID + base_id: Base ID (required for v3) + + Returns: + API endpoint path + """ + return self.records_list(table_id, base_id) + + def records_update(self, table_id: str, base_id: str | None = None) -> str: + """Build path for updating records. + + Args: + table_id: Table ID + base_id: Base ID (required for v3) + + Returns: + API endpoint path + """ + return self.records_list(table_id, base_id) + + def records_delete(self, table_id: str, base_id: str | None = None) -> str: + """Build path for deleting records. + + Args: + table_id: Table ID + base_id: Base ID (required for v3) + + Returns: + API endpoint path + """ + return self.records_list(table_id, base_id) + + def records_count(self, table_id: str, base_id: str | None = None) -> str: + """Build path for counting records. + + Args: + table_id: Table ID + base_id: Base ID (required for v3) + + Returns: + API endpoint path + """ + if self.api_version == APIVersion.V2: + return f"api/v2/tables/{table_id}/records/count" + else: # V3 + if not base_id: + raise ValueError("base_id is required for API v3") + return f"api/v3/data/{base_id}/{table_id}/count" + + def table_get(self, table_id: str, base_id: str | None = None) -> str: + """Build path for getting table metadata. + + Args: + table_id: Table ID + base_id: Base ID (required for v3) + + Returns: + API endpoint path + """ + if self.api_version == APIVersion.V2: + return f"api/v2/meta/tables/{table_id}" + else: # V3 + if not base_id: + raise ValueError("base_id is required for API v3") + return f"api/v3/meta/bases/{base_id}/tables/{table_id}" + + def tables_list(self, base_id: str) -> str: + """Build path for listing tables. + + Args: + base_id: Base ID + + Returns: + API endpoint path + """ + if self.api_version == APIVersion.V2: + return f"api/v2/meta/bases/{base_id}/tables" + else: # V3 + return f"api/v3/meta/bases/{base_id}/tables" + + def table_create(self, base_id: str) -> str: + """Build path for creating a table. + + Args: + base_id: Base ID + + Returns: + API endpoint path + """ + return self.tables_list(base_id) + + def table_update(self, table_id: str, base_id: str | None = None) -> str: + """Build path for updating a table. + + Args: + table_id: Table ID + base_id: Base ID (required for v3) + + Returns: + API endpoint path + """ + return self.table_get(table_id, base_id) + + def table_delete(self, table_id: str, base_id: str | None = None) -> str: + """Build path for deleting a table. + + Args: + table_id: Table ID + base_id: Base ID (required for v3) + + Returns: + API endpoint path + """ + return self.table_get(table_id, base_id) + + def links_list( + self, + table_id: str, + link_field_id: str, + record_id: str, + base_id: str | None = None, + ) -> str: + """Build path for listing linked records. + + Args: + table_id: Table ID + link_field_id: Link field ID + record_id: Record ID + base_id: Base ID (required for v3) + + Returns: + API endpoint path + """ + if self.api_version == APIVersion.V2: + return f"api/v2/tables/{table_id}/links/{link_field_id}/records/{record_id}" + else: # V3 + if not base_id: + raise ValueError("base_id is required for API v3") + return f"api/v3/data/{base_id}/{table_id}/links/{link_field_id}/{record_id}" + + def links_create( + self, + table_id: str, + link_field_id: str, + record_id: str, + base_id: str | None = None, + ) -> str: + """Build path for creating links. + + Args: + table_id: Table ID + link_field_id: Link field ID + record_id: Record ID + base_id: Base ID (required for v3) + + Returns: + API endpoint path + """ + return self.links_list(table_id, link_field_id, record_id, base_id) + + def links_delete( + self, + table_id: str, + link_field_id: str, + record_id: str, + base_id: str | None = None, + ) -> str: + """Build path for deleting links. + + Args: + table_id: Table ID + link_field_id: Link field ID + record_id: Record ID + base_id: Base ID (required for v3) + + Returns: + API endpoint path + """ + return self.links_list(table_id, link_field_id, record_id, base_id) + + def file_upload(self, table_id: str, base_id: str | None = None) -> str: + """Build path for file upload. + + Args: + table_id: Table ID + base_id: Base ID (required for v3) + + Returns: + API endpoint path + """ + if self.api_version == APIVersion.V2: + return f"api/v2/tables/{table_id}/attachments" + else: # V3 + if not base_id: + raise ValueError("base_id is required for API v3") + return f"api/v3/data/{base_id}/{table_id}/attachments" + + # ======================================================================== + # META API PATHS + # ======================================================================== + + def bases_list(self) -> str: + """Build path for listing bases. + + Returns: + API endpoint path + """ + if self.api_version == APIVersion.V2: + return "api/v2/meta/bases" + else: # V3 + return "api/v3/meta/bases" + + def base_get(self, base_id: str) -> str: + """Build path for getting base information. + + Args: + base_id: Base ID + + Returns: + API endpoint path + """ + if self.api_version == APIVersion.V2: + return f"api/v2/meta/bases/{base_id}" + else: # V3 + return f"api/v3/meta/bases/{base_id}" + + def tables_list_meta(self, base_id: str) -> str: + """Build path for listing tables (meta endpoint). + + Args: + base_id: Base ID + + Returns: + API endpoint path + """ + if self.api_version == APIVersion.V2: + return f"api/v2/meta/bases/{base_id}/tables" + else: # V3 + return f"api/v3/meta/bases/{base_id}/tables" + + def table_get_meta(self, table_id: str, base_id: str | None = None) -> str: + """Build path for getting table metadata. + + Args: + table_id: Table ID + base_id: Base ID (required for v3) + + Returns: + API endpoint path + """ + if self.api_version == APIVersion.V2: + return f"api/v2/meta/tables/{table_id}" + else: # V3 + if not base_id: + raise ValueError("base_id is required for API v3") + return f"api/v3/meta/bases/{base_id}/tables/{table_id}" + + def column_get(self, column_id: str, base_id: str | None = None) -> str: + """Build path for column/field operations. + + Args: + column_id: Column/Field ID + base_id: Base ID (required for v3) + + Returns: + API endpoint path + """ + if self.api_version == APIVersion.V2: + return f"api/v2/meta/columns/{column_id}" + else: # V3 - columns become fields + if not base_id: + raise ValueError("base_id is required for API v3") + return f"api/v3/meta/bases/{base_id}/fields/{column_id}" + + def columns_create(self, table_id: str, base_id: str | None = None) -> str: + """Build path for creating columns/fields. + + Args: + table_id: Table ID + base_id: Base ID (required for v3) + + Returns: + API endpoint path + """ + if self.api_version == APIVersion.V2: + return f"api/v2/meta/tables/{table_id}/columns" + else: # V3 + if not base_id: + raise ValueError("base_id is required for API v3") + return f"api/v3/meta/bases/{base_id}/tables/{table_id}/fields" + + def view_get(self, view_id: str, base_id: str | None = None) -> str: + """Build path for view operations. + + Args: + view_id: View ID + base_id: Base ID (required for v3) + + Returns: + API endpoint path + """ + if self.api_version == APIVersion.V2: + return f"api/v2/meta/views/{view_id}" + else: # V3 + if not base_id: + raise ValueError("base_id is required for API v3") + return f"api/v3/meta/bases/{base_id}/views/{view_id}" + + def views_list(self, table_id: str, base_id: str | None = None) -> str: + """Build path for listing views. + + Args: + table_id: Table ID + base_id: Base ID (required for v3) + + Returns: + API endpoint path + """ + if self.api_version == APIVersion.V2: + return f"api/v2/meta/tables/{table_id}/views" + else: # V3 + if not base_id: + raise ValueError("base_id is required for API v3") + return f"api/v3/meta/bases/{base_id}/tables/{table_id}/views" + + def webhook_get(self, webhook_id: str, base_id: str | None = None) -> str: + """Build path for webhook operations. + + Args: + webhook_id: Webhook ID + base_id: Base ID (required for v3) + + Returns: + API endpoint path + """ + if self.api_version == APIVersion.V2: + return f"api/v2/meta/hooks/{webhook_id}" + else: # V3 + if not base_id: + raise ValueError("base_id is required for API v3") + return f"api/v3/meta/bases/{base_id}/hooks/{webhook_id}" + + def webhooks_list(self, table_id: str, base_id: str | None = None) -> str: + """Build path for listing webhooks. + + Args: + table_id: Table ID + base_id: Base ID (required for v3) + + Returns: + API endpoint path + """ + if self.api_version == APIVersion.V2: + return f"api/v2/meta/tables/{table_id}/hooks" + else: # V3 + if not base_id: + raise ValueError("base_id is required for API v3") + return f"api/v3/meta/bases/{base_id}/tables/{table_id}/hooks" diff --git a/src/nocodb_simple_client/base_resolver.py b/src/nocodb_simple_client/base_resolver.py new file mode 100644 index 0000000..34dc427 --- /dev/null +++ b/src/nocodb_simple_client/base_resolver.py @@ -0,0 +1,164 @@ +"""Base ID resolution and caching for NocoDB API v3. + +MIT License + +Copyright (c) BAUER GROUP + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .client import NocoDBClient + + +class BaseIdResolver: + """Resolver for mapping table IDs to base IDs in v3 API. + + In NocoDB API v3, all endpoints require a baseId in the path. + This resolver caches the mapping between table IDs and base IDs + to avoid repeated API calls. + + Example: + >>> resolver = BaseIdResolver(client) + >>> base_id = resolver.get_base_id("table_abc123") + >>> # Subsequent calls use cached value + >>> base_id = resolver.get_base_id("table_abc123") # No API call + """ + + def __init__(self, client: "NocoDBClient"): + """Initialize the base ID resolver. + + Args: + client: The NocoDB client instance + """ + self._client = client + self._cache: dict[str, str] = {} # table_id -> base_id + self._enabled = True + + def get_base_id(self, table_id: str, force_refresh: bool = False) -> str: + """Get the base ID for a given table ID. + + Args: + table_id: The table ID to resolve + force_refresh: Force a cache refresh even if value exists + + Returns: + The base ID for the table + + Raises: + TableNotFoundException: If the table doesn't exist + NocoDBException: If the API call fails + """ + # Check cache first + if not force_refresh and table_id in self._cache: + return self._cache[table_id] + + # Fetch table metadata to get base_id + # This uses the v2 endpoint which doesn't require base_id + table_info = self._client._get(f"api/v2/meta/tables/{table_id}") + + if not table_info or "base_id" not in table_info: + # Try alternative response structure + if "source_id" in table_info: + # In some NocoDB versions, it's called source_id + base_id = table_info["source_id"] + elif "project_id" in table_info: + # Or project_id in older versions + base_id = table_info["project_id"] + else: + # If we can't find it, try to extract from fk_model_id or similar + # As a fallback, we might need to list all bases and find the table + base_id = self._find_base_id_from_list(table_id) + else: + base_id = table_info["base_id"] + + # Cache the result + self._cache[table_id] = base_id + return base_id + + def _find_base_id_from_list(self, table_id: str) -> str: + """Fallback method to find base_id by listing tables in all bases. + + Args: + table_id: The table ID to find + + Returns: + The base ID containing this table + + Raises: + TableNotFoundException: If table not found in any base + """ + from .exceptions import TableNotFoundException + + # This is a more expensive operation, should rarely be needed + # We'd need to implement listing bases first + # For now, raise an error with helpful message + raise TableNotFoundException( + f"Could not resolve base_id for table {table_id}. " + "Please provide base_id explicitly when using API v3." + ) + + def set_base_id(self, table_id: str, base_id: str) -> None: + """Manually set a base ID mapping. + + Useful when you already know the base_id and want to avoid API calls. + + Args: + table_id: The table ID + base_id: The base ID + """ + self._cache[table_id] = base_id + + def clear_cache(self, table_id: str | None = None) -> None: + """Clear the cache. + + Args: + table_id: If provided, clear only this table's cache. + If None, clear all cache. + """ + if table_id: + self._cache.pop(table_id, None) + else: + self._cache.clear() + + def get_cache_size(self) -> int: + """Get the number of cached mappings. + + Returns: + Number of table_id -> base_id mappings in cache + """ + return len(self._cache) + + def disable(self) -> None: + """Disable the resolver (will raise errors when base_id needed).""" + self._enabled = False + + def enable(self) -> None: + """Enable the resolver.""" + self._enabled = True + + def is_enabled(self) -> bool: + """Check if resolver is enabled. + + Returns: + True if enabled, False otherwise + """ + return self._enabled diff --git a/src/nocodb_simple_client/client.py b/src/nocodb_simple_client/client.py index cadf356..12abe84 100644 --- a/src/nocodb_simple_client/client.py +++ b/src/nocodb_simple_client/client.py @@ -25,7 +25,7 @@ import mimetypes from pathlib import Path -from typing import TYPE_CHECKING, Any, Optional, Union +from typing import TYPE_CHECKING, Any, Union if TYPE_CHECKING: from .config import NocoDBConfig @@ -33,6 +33,8 @@ import requests from requests_toolbelt.multipart.encoder import MultipartEncoder +from .api_version import APIVersion, PathBuilder, QueryParamAdapter +from .base_resolver import BaseIdResolver from .exceptions import NocoDBException, RecordNotFoundException, ValidationException @@ -50,17 +52,29 @@ class NocoDBClient: (defaults to "X-BAUERGROUP-Auth") max_redirects (int, optional): Maximum number of redirects to follow timeout (int, optional): Request timeout in seconds + api_version (str, optional): API version to use ("v2" or "v3", defaults to "v2") + base_id (str, optional): Default base ID for v3 API operations Attributes: headers (Dict[str, str]): HTTP headers used for requests + api_version (APIVersion): The API version being used + base_id (str, optional): Default base ID for v3 operations Example: - >>> # Default usage + >>> # Default usage (v2 API) >>> client = NocoDBClient( ... base_url="https://app.nocodb.com", ... db_auth_token="your-api-token" ... ) >>> + >>> # Using v3 API with base_id + >>> client = NocoDBClient( + ... base_url="https://app.nocodb.com", + ... db_auth_token="your-api-token", + ... api_version="v3", + ... base_id="base_abc123" + ... ) + >>> >>> # With custom protection header >>> client = NocoDBClient( ... base_url="https://app.nocodb.com", @@ -78,7 +92,9 @@ def __init__( access_protection_header: str = "X-BAUERGROUP-Auth", max_redirects: int | None = None, timeout: int | None = None, - config: Optional["NocoDBConfig"] = None, + config: "NocoDBConfig | None" = None, + api_version: str = "v2", + base_id: str | None = None, ) -> None: from .config import NocoDBConfig # Import here to avoid circular import @@ -135,6 +151,49 @@ def __init__( if max_redirects is not None: self._session.max_redirects = max_redirects + # API version support + self.api_version = APIVersion(api_version) + self.base_id = base_id + self._path_builder = PathBuilder(self.api_version) + self._param_adapter = QueryParamAdapter() + + # Base ID resolver for v3 API (resolves table_id -> base_id) + self._base_resolver = BaseIdResolver(self) if self.api_version == APIVersion.V3 else None + + def _resolve_base_id(self, table_id: str, base_id: str | None = None) -> str: + """Resolve base_id for API v3 operations. + + Args: + table_id: The table ID + base_id: Optional base_id override + + Returns: + The resolved base_id + + Raises: + ValueError: If base_id cannot be resolved for v3 API + """ + # If base_id provided explicitly, use it + if base_id: + return base_id + + # Use client's default base_id if set + if self.base_id: + return self.base_id + + # For v3, try to resolve from table_id + if self.api_version == APIVersion.V3 and self._base_resolver: + return self._base_resolver.get_base_id(table_id) + + # For v2, base_id is not required + if self.api_version == APIVersion.V2: + raise ValueError("base_id should not be required for API v2") + + raise ValueError( + f"base_id is required for API v3. Please provide it in the client constructor " + f"or as a parameter to the method call for table {table_id}" + ) + def _get(self, endpoint: str, params: dict[str, Any] | None = None) -> dict[str, Any]: """Make a GET request to the API.""" url = f"{self._base_url}/{endpoint}" @@ -236,6 +295,7 @@ def _check_for_error(self, response: requests.Response) -> None: def get_records( self, table_id: str, + base_id: str | None = None, sort: str | None = None, where: str | None = None, fields: list[str] | None = None, @@ -245,6 +305,7 @@ def get_records( Args: table_id: The ID of the table + base_id: Base ID (required for v3, optional for v2) sort: Sort criteria (e.g., "Id", "-CreatedAt") where: Filter condition (e.g., "(Name,eq,John)") fields: List of fields to retrieve @@ -257,6 +318,14 @@ def get_records( RecordNotFoundException: If no records match the criteria NocoDBException: For other API errors """ + # Resolve base_id for v3 + resolved_base_id = None + if self.api_version == APIVersion.V3: + resolved_base_id = self._resolve_base_id(table_id, base_id) + + # Build path using PathBuilder + endpoint = self._path_builder.records_list(table_id, resolved_base_id) + records = [] offset = 0 remaining_limit = limit @@ -270,7 +339,13 @@ def get_records( # Remove None values from params params = {k: v for k, v in params.items() if v is not None} - response = self._get(f"api/v2/tables/{table_id}/records", params=params) + # Convert query parameters for v3 + if self.api_version == APIVersion.V3: + params = self._param_adapter.convert_pagination_to_v3(params) + if sort: + params["sort"] = self._param_adapter.convert_sort_to_v3(sort) + + response = self._get(endpoint, params=params) batch_records = response.get("list", []) records.extend(batch_records) @@ -288,6 +363,7 @@ def get_record( self, table_id: str, record_id: int | str, + base_id: str | None = None, fields: list[str] | None = None, ) -> dict[str, Any]: """Get a single record by ID. @@ -295,6 +371,7 @@ def get_record( Args: table_id: The ID of the table record_id: The ID of the record + base_id: Base ID (required for v3, optional for v2) fields: List of fields to retrieve Returns: @@ -304,18 +381,29 @@ def get_record( RecordNotFoundException: If the record is not found NocoDBException: For other API errors """ + # Resolve base_id for v3 + resolved_base_id = None + if self.api_version == APIVersion.V3: + resolved_base_id = self._resolve_base_id(table_id, base_id) + + # Build path using PathBuilder + endpoint = self._path_builder.records_get(table_id, str(record_id), resolved_base_id) + params = {} if fields: params["fields"] = ",".join(fields) - return self._get(f"api/v2/tables/{table_id}/records/{record_id}", params=params) + return self._get(endpoint, params=params) - def insert_record(self, table_id: str, record: dict[str, Any]) -> int | str: + def insert_record( + self, table_id: str, record: dict[str, Any], base_id: str | None = None + ) -> int | str: """Insert a new record into a table. Args: table_id: The ID of the table record: Dictionary containing the record data + base_id: Base ID (required for v3, optional for v2) Returns: The ID of the inserted record @@ -323,7 +411,15 @@ def insert_record(self, table_id: str, record: dict[str, Any]) -> int | str: Raises: NocoDBException: For API errors """ - response = self._post(f"api/v2/tables/{table_id}/records", data=record) + # Resolve base_id for v3 + resolved_base_id = None + if self.api_version == APIVersion.V3: + resolved_base_id = self._resolve_base_id(table_id, base_id) + + # Build path using PathBuilder + endpoint = self._path_builder.records_create(table_id, resolved_base_id) + + response = self._post(endpoint, data=record) # API v2 returns a single object: {"Id": 123} if isinstance(response, dict): record_id = response.get("Id") @@ -344,6 +440,7 @@ def update_record( table_id: str, record: dict[str, Any], record_id: int | str | None = None, + base_id: str | None = None, ) -> int | str: """Update an existing record. @@ -351,6 +448,7 @@ def update_record( table_id: The ID of the table record: Dictionary containing the updated record data record_id: The ID of the record to update (optional if included in record) + base_id: Base ID (required for v3, optional for v2) Returns: The ID of the updated record @@ -362,7 +460,15 @@ def update_record( if record_id is not None: record["Id"] = record_id - response = self._patch(f"api/v2/tables/{table_id}/records", data=record) + # Resolve base_id for v3 + resolved_base_id = None + if self.api_version == APIVersion.V3: + resolved_base_id = self._resolve_base_id(table_id, base_id) + + # Build path using PathBuilder + endpoint = self._path_builder.records_update(table_id, resolved_base_id) + + response = self._patch(endpoint, data=record) if isinstance(response, dict): record_id = response.get("Id") else: @@ -377,12 +483,15 @@ def update_record( ) return record_id # type: ignore[no-any-return] - def delete_record(self, table_id: str, record_id: int | str) -> int | str: + def delete_record( + self, table_id: str, record_id: int | str, base_id: str | None = None + ) -> int | str: """Delete a record from a table. Args: table_id: The ID of the table record_id: The ID of the record to delete + base_id: Base ID (required for v3, optional for v2) Returns: The ID of the deleted record @@ -391,8 +500,15 @@ def delete_record(self, table_id: str, record_id: int | str) -> int | str: RecordNotFoundException: If the record is not found NocoDBException: For other API errors """ + # Resolve base_id for v3 + resolved_base_id = None + if self.api_version == APIVersion.V3: + resolved_base_id = self._resolve_base_id(table_id, base_id) + + # Build path using PathBuilder + endpoint = self._path_builder.records_delete(table_id, resolved_base_id) - response = self._delete(f"api/v2/tables/{table_id}/records", data={"Id": record_id}) + response = self._delete(endpoint, data={"Id": record_id}) if isinstance(response, dict): deleted_id = response.get("Id") else: @@ -407,12 +523,15 @@ def delete_record(self, table_id: str, record_id: int | str) -> int | str: ) return deleted_id # type: ignore[no-any-return] - def count_records(self, table_id: str, where: str | None = None) -> int: + def count_records( + self, table_id: str, where: str | None = None, base_id: str | None = None + ) -> int: """Count records in a table. Args: table_id: The ID of the table where: Filter condition (e.g., "(Name,eq,John)") + base_id: Base ID (required for v3, optional for v2) Returns: Number of records matching the criteria @@ -420,20 +539,31 @@ def count_records(self, table_id: str, where: str | None = None) -> int: Raises: NocoDBException: For API errors """ + # Resolve base_id for v3 + resolved_base_id = None + if self.api_version == APIVersion.V3: + resolved_base_id = self._resolve_base_id(table_id, base_id) + + # Build path using PathBuilder + endpoint = self._path_builder.records_count(table_id, resolved_base_id) + params = {} if where: params["where"] = where - response = self._get(f"api/v2/tables/{table_id}/records/count", params=params) + response = self._get(endpoint, params=params) count = response.get("count", 0) return int(count) if count is not None else 0 - def bulk_insert_records(self, table_id: str, records: list[dict[str, Any]]) -> list[int | str]: + def bulk_insert_records( + self, table_id: str, records: list[dict[str, Any]], base_id: str | None = None + ) -> list[int | str]: """Insert multiple records at once for better performance. Args: table_id: The ID of the table records: List of record dictionaries to insert + base_id: Base ID (required for v3, optional for v2) Returns: List of inserted record IDs @@ -448,9 +578,17 @@ def bulk_insert_records(self, table_id: str, records: list[dict[str, Any]]) -> l if not isinstance(records, list): raise ValidationException("Records must be a list") + # Resolve base_id for v3 + resolved_base_id = None + if self.api_version == APIVersion.V3: + resolved_base_id = self._resolve_base_id(table_id, base_id) + + # Build path using PathBuilder + endpoint = self._path_builder.records_create(table_id, resolved_base_id) + # NocoDB v2 API supports bulk insert via array payload try: - response = self._post(f"api/v2/tables/{table_id}/records", data=records) + response = self._post(endpoint, data=records) # Response should be list of record IDs if isinstance(response, list): @@ -472,12 +610,15 @@ def bulk_insert_records(self, table_id: str, records: list[dict[str, Any]]) -> l raise raise NocoDBException("BULK_INSERT_ERROR", f"Bulk insert failed: {str(e)}") from e - def bulk_update_records(self, table_id: str, records: list[dict[str, Any]]) -> list[int | str]: + def bulk_update_records( + self, table_id: str, records: list[dict[str, Any]], base_id: str | None = None + ) -> list[int | str]: """Update multiple records at once for better performance. Args: table_id: The ID of the table records: List of record dictionaries to update (must include Id field) + base_id: Base ID (required for v3, optional for v2) Returns: List of updated record IDs @@ -499,8 +640,16 @@ def bulk_update_records(self, table_id: str, records: list[dict[str, Any]]) -> l if "Id" not in record: raise ValidationException(f"Record at index {i} missing required 'Id' field") + # Resolve base_id for v3 + resolved_base_id = None + if self.api_version == APIVersion.V3: + resolved_base_id = self._resolve_base_id(table_id, base_id) + + # Build path using PathBuilder + endpoint = self._path_builder.records_update(table_id, resolved_base_id) + try: - response = self._patch(f"api/v2/tables/{table_id}/records", data=records) + response = self._patch(endpoint, data=records) # Response should be list of record IDs if isinstance(response, list): @@ -522,12 +671,15 @@ def bulk_update_records(self, table_id: str, records: list[dict[str, Any]]) -> l raise raise NocoDBException("BULK_UPDATE_ERROR", f"Bulk update failed: {str(e)}") from e - def bulk_delete_records(self, table_id: str, record_ids: list[int | str]) -> list[int | str]: + def bulk_delete_records( + self, table_id: str, record_ids: list[int | str], base_id: str | None = None + ) -> list[int | str]: """Delete multiple records at once for better performance. Args: table_id: The ID of the table record_ids: List of record IDs to delete + base_id: Base ID (required for v3, optional for v2) Returns: List of deleted record IDs @@ -542,11 +694,19 @@ def bulk_delete_records(self, table_id: str, record_ids: list[int | str]) -> lis if not isinstance(record_ids, list): raise ValidationException("Record IDs must be a list") + # Resolve base_id for v3 + resolved_base_id = None + if self.api_version == APIVersion.V3: + resolved_base_id = self._resolve_base_id(table_id, base_id) + + # Build path using PathBuilder + endpoint = self._path_builder.records_delete(table_id, resolved_base_id) + # Convert to list of dictionaries with Id field records_to_delete = [{"Id": record_id} for record_id in record_ids] try: - response = self._delete(f"api/v2/tables/{table_id}/records", data=records_to_delete) + response = self._delete(endpoint, data=records_to_delete) # Response should be list of record IDs if isinstance(response, list): @@ -585,12 +745,13 @@ def _multipart_post( self._check_for_error(response) return response.json() # type: ignore[no-any-return] - def _upload_file(self, table_id: str, file_path: str | Path) -> Any: + def _upload_file(self, table_id: str, file_path: str | Path, base_id: str | None = None) -> Any: """Upload a file to NocoDB storage. Args: table_id: The ID of the table file_path: Path to the file to upload + base_id: Base ID (required for v3, optional for v2) Returns: Upload response with file information @@ -608,10 +769,18 @@ def _upload_file(self, table_id: str, file_path: str | Path) -> Any: if mime_type is None: mime_type = "application/octet-stream" + # Resolve base_id for v3 + resolved_base_id = None + if self.api_version == APIVersion.V3: + resolved_base_id = self._resolve_base_id(table_id, base_id) + + # Build upload endpoint + endpoint = self._path_builder.file_upload(table_id, resolved_base_id) + with file_path.open("rb") as f: files = {"file": (filename, f, mime_type)} path = f"files/{table_id}" - return self._multipart_post("api/v2/storage/upload", files, fields={"path": path}) + return self._multipart_post(endpoint, files, fields={"path": path}) def attach_file_to_record( self, @@ -619,6 +788,7 @@ def attach_file_to_record( record_id: int | str, field_name: str, file_path: str | Path, + base_id: str | None = None, ) -> int | str: """Attach a file to a record without overwriting existing files. @@ -627,6 +797,7 @@ def attach_file_to_record( record_id: The ID of the record field_name: The name of the attachment field file_path: Path to the file to attach + base_id: Base ID (required for v3, optional for v2) Returns: The ID of the updated record @@ -635,7 +806,7 @@ def attach_file_to_record( RecordNotFoundException: If the record is not found NocoDBException: For other API errors """ - return self.attach_files_to_record(table_id, record_id, field_name, [file_path]) + return self.attach_files_to_record(table_id, record_id, field_name, [file_path], base_id) def attach_files_to_record( self, @@ -643,6 +814,7 @@ def attach_files_to_record( record_id: int | str, field_name: str, file_paths: list[str | Path], + base_id: str | None = None, ) -> int | str: """Attach multiple files to a record without overwriting existing files. @@ -651,6 +823,7 @@ def attach_files_to_record( record_id: The ID of the record field_name: The name of the attachment field file_paths: List of file paths to attach + base_id: Base ID (required for v3, optional for v2) Returns: The ID of the updated record @@ -659,11 +832,11 @@ def attach_files_to_record( RecordNotFoundException: If the record is not found NocoDBException: For other API errors """ - existing_record = self.get_record(table_id, record_id, fields=[field_name]) + existing_record = self.get_record(table_id, record_id, base_id=base_id, fields=[field_name]) existing_files = existing_record.get(field_name, []) or [] for file_path in file_paths: - upload_response = self._upload_file(table_id, file_path) + upload_response = self._upload_file(table_id, file_path, base_id) # NocoDB upload returns an array of file objects if isinstance(upload_response, list): existing_files.extend(upload_response) @@ -673,13 +846,14 @@ def attach_files_to_record( raise NocoDBException("INVALID_RESPONSE", "Invalid upload response format") record_update = {field_name: existing_files} - return self.update_record(table_id, record_update, record_id) + return self.update_record(table_id, record_update, record_id, base_id) def delete_file_from_record( self, table_id: str, record_id: int | str, field_name: str, + base_id: str | None = None, ) -> int | str: """Delete all files from a record field. @@ -687,6 +861,7 @@ def delete_file_from_record( table_id: The ID of the table record_id: The ID of the record field_name: The name of the attachment field + base_id: Base ID (required for v3, optional for v2) Returns: The ID of the updated record @@ -696,7 +871,7 @@ def delete_file_from_record( NocoDBException: For other API errors """ record = {field_name: "[]"} - return self.update_record(table_id, record, record_id) + return self.update_record(table_id, record, record_id, base_id) def _download_single_file(self, file_info: dict[str, Any], file_path: Path) -> None: """Helper method to download a single file. @@ -734,6 +909,7 @@ def download_file_from_record( record_id: int | str, field_name: str, file_path: str | Path, + base_id: str | None = None, ) -> None: """Download the first file from a record field. @@ -742,12 +918,13 @@ def download_file_from_record( record_id: The ID of the record field_name: The name of the attachment field file_path: Path where the file should be saved + base_id: Base ID (required for v3, optional for v2) Raises: RecordNotFoundException: If the record is not found NocoDBException: If no files are found or download fails """ - record = self.get_record(table_id, record_id, fields=[field_name]) + record = self.get_record(table_id, record_id, base_id=base_id, fields=[field_name]) if field_name not in record or not record[field_name]: raise NocoDBException("FILE_NOT_FOUND", "No file found in the specified field.") @@ -761,6 +938,7 @@ def download_files_from_record( record_id: int | str, field_name: str, directory: str | Path, + base_id: str | None = None, ) -> None: """Download all files from a record field. @@ -769,12 +947,13 @@ def download_files_from_record( record_id: The ID of the record field_name: The name of the attachment field directory: Directory where files should be saved + base_id: Base ID (required for v3, optional for v2) Raises: RecordNotFoundException: If the record is not found NocoDBException: If no files are found or download fails """ - record = self.get_record(table_id, record_id, fields=[field_name]) + record = self.get_record(table_id, record_id, base_id=base_id, fields=[field_name]) if field_name not in record or not record[field_name]: raise NocoDBException("FILE_NOT_FOUND", "No files found in the specified field.") diff --git a/src/nocodb_simple_client/meta_client.py b/src/nocodb_simple_client/meta_client.py index ccd9624..8f82c18 100644 --- a/src/nocodb_simple_client/meta_client.py +++ b/src/nocodb_simple_client/meta_client.py @@ -86,6 +86,8 @@ def __init__(self, config: NocoDBConfig | None = None, **kwargs: Any) -> None: def list_workspaces(self) -> list[dict[str, Any]]: """List all workspaces accessible to the authenticated user. + Supports both API v2 and v3. + Returns: List of workspace metadata dictionaries @@ -97,13 +99,17 @@ def list_workspaces(self) -> list[dict[str, Any]]: >>> for workspace in workspaces: ... print(workspace['id'], workspace['title']) """ - response = self._get("api/v2/meta/workspaces") + # Workspace endpoints are same for v2 and v3 + endpoint = f"api/{self.api_version}/meta/workspaces" + response = self._get(endpoint) workspace_list = response.get("list", []) return workspace_list if isinstance(workspace_list, list) else [] def get_workspace(self, workspace_id: str) -> dict[str, Any]: """Get detailed information about a specific workspace. + Supports both API v2 and v3. + Args: workspace_id: The workspace ID @@ -118,12 +124,15 @@ def get_workspace(self, workspace_id: str) -> dict[str, Any]: >>> workspace = meta_client.get_workspace("ws_abc123") >>> print(workspace['title'], workspace['created_at']) """ - result = self._get(f"api/v2/meta/workspaces/{workspace_id}") + endpoint = f"api/{self.api_version}/meta/workspaces/{workspace_id}" + result = self._get(endpoint) return result if isinstance(result, dict) else {"data": result} def create_workspace(self, workspace_data: dict[str, Any]) -> dict[str, Any]: """Create a new workspace. + Supports both API v2 and v3. + Args: workspace_data: Workspace creation data (title, description, etc.) @@ -141,12 +150,15 @@ def create_workspace(self, workspace_data: dict[str, Any]) -> dict[str, Any]: ... } >>> workspace = meta_client.create_workspace(workspace_data) """ - result = self._post("api/v2/meta/workspaces", data=workspace_data) + endpoint = f"api/{self.api_version}/meta/workspaces" + result = self._post(endpoint, data=workspace_data) return result if isinstance(result, dict) else {"data": result} def update_workspace(self, workspace_id: str, workspace_data: dict[str, Any]) -> dict[str, Any]: """Update workspace metadata. + Supports both API v2 and v3. + Args: workspace_id: The workspace ID to update workspace_data: Updated workspace data @@ -164,7 +176,8 @@ def update_workspace(self, workspace_id: str, workspace_data: dict[str, Any]) -> ... {"title": "Updated Workspace Name"} ... ) """ - result = self._patch(f"api/v2/meta/workspaces/{workspace_id}", data=workspace_data) + endpoint = f"api/{self.api_version}/meta/workspaces/{workspace_id}" + result = self._patch(endpoint, data=workspace_data) return result if isinstance(result, dict) else {"data": result} def delete_workspace(self, workspace_id: str) -> dict[str, Any]: @@ -172,6 +185,8 @@ def delete_workspace(self, workspace_id: str) -> dict[str, Any]: Warning: This will delete all bases and data within the workspace. + Supports both API v2 and v3. + Args: workspace_id: The workspace ID to delete @@ -185,7 +200,8 @@ def delete_workspace(self, workspace_id: str) -> dict[str, Any]: Example: >>> result = meta_client.delete_workspace("ws_abc123") """ - result = self._delete(f"api/v2/meta/workspaces/{workspace_id}") + endpoint = f"api/{self.api_version}/meta/workspaces/{workspace_id}" + result = self._delete(endpoint) return result if isinstance(result, dict) else {"data": result} # ======================================================================== @@ -195,6 +211,8 @@ def delete_workspace(self, workspace_id: str) -> dict[str, Any]: def list_bases(self) -> list[dict[str, Any]]: """List all bases. + Supports both API v2 and v3. + Returns: List of base metadata dictionaries @@ -206,13 +224,16 @@ def list_bases(self) -> list[dict[str, Any]]: >>> for base in bases: ... print(base['id'], base['title']) """ - response = self._get("api/v2/meta/bases/") + endpoint = self._path_builder.bases_list() + response = self._get(endpoint) base_list = response.get("list", []) return base_list if isinstance(base_list, list) else [] def get_base(self, base_id: str) -> dict[str, Any]: """Get detailed information about a specific base. + Supports both API v2 and v3. + Args: base_id: The base ID @@ -227,12 +248,15 @@ def get_base(self, base_id: str) -> dict[str, Any]: >>> base = meta_client.get_base("p_abc123") >>> print(base['title'], base['status']) """ - result = self._get(f"api/v2/meta/bases/{base_id}") + endpoint = self._path_builder.base_get(base_id) + result = self._get(endpoint) return result if isinstance(result, dict) else {"data": result} def create_base(self, workspace_id: str, base_data: dict[str, Any]) -> dict[str, Any]: """Create a new base in a workspace. + Supports both API v2 and v3. + Args: workspace_id: The workspace ID where base will be created base_data: Base creation data (title, description, etc.) @@ -251,12 +275,15 @@ def create_base(self, workspace_id: str, base_data: dict[str, Any]) -> dict[str, ... } >>> base = meta_client.create_base("ws_abc123", base_data) """ - result = self._post(f"api/v2/meta/workspaces/{workspace_id}/bases", data=base_data) + endpoint = f"api/{self.api_version}/meta/workspaces/{workspace_id}/bases" + result = self._post(endpoint, data=base_data) return result if isinstance(result, dict) else {"data": result} def update_base(self, base_id: str, base_data: dict[str, Any]) -> dict[str, Any]: """Update base metadata. + Supports both API v2 and v3. + Args: base_id: The base ID to update base_data: Updated base data @@ -274,7 +301,8 @@ def update_base(self, base_id: str, base_data: dict[str, Any]) -> dict[str, Any] ... {"title": "Updated Project Name"} ... ) """ - result = self._patch(f"api/v2/meta/bases/{base_id}", data=base_data) + endpoint = self._path_builder.base_get(base_id) + result = self._patch(endpoint, data=base_data) return result if isinstance(result, dict) else {"data": result} def delete_base(self, base_id: str) -> dict[str, Any]: @@ -282,6 +310,8 @@ def delete_base(self, base_id: str) -> dict[str, Any]: Warning: This will delete all tables and data within the base. + Supports both API v2 and v3. + Args: base_id: The base ID to delete @@ -295,7 +325,8 @@ def delete_base(self, base_id: str) -> dict[str, Any]: Example: >>> result = meta_client.delete_base("p_abc123") """ - result = self._delete(f"api/v2/meta/bases/{base_id}") + endpoint = self._path_builder.base_get(base_id) + result = self._delete(endpoint) return result if isinstance(result, dict) else {"data": result} # ======================================================================== @@ -305,6 +336,8 @@ def delete_base(self, base_id: str) -> dict[str, Any]: def list_tables(self, base_id: str) -> list[dict[str, Any]]: """List all tables in a base. + Supports both API v2 and v3. + Args: base_id: The base ID @@ -315,15 +348,19 @@ def list_tables(self, base_id: str) -> list[dict[str, Any]]: NocoDBException: For API errors ValidationException: If base_id is invalid """ - response = self._get(f"api/v2/meta/bases/{base_id}/tables") + endpoint = self._path_builder.tables_list_meta(base_id) + response = self._get(endpoint) table_list = response.get("list", []) return table_list if isinstance(table_list, list) else [] - def get_table_info(self, table_id: str) -> dict[str, Any]: + def get_table_info(self, table_id: str, base_id: str | None = None) -> dict[str, Any]: """Get table metadata information. + Supports both API v2 and v3. + Args: table_id: The table ID + base_id: Base ID (required for v3, optional for v2) Returns: Table metadata dictionary containing schema, columns, relationships @@ -332,12 +369,22 @@ def get_table_info(self, table_id: str) -> dict[str, Any]: NocoDBException: For API errors TableNotFoundException: If table is not found """ - result = self._get(f"api/v2/meta/tables/{table_id}") + # Resolve base_id for v3 + from .api_version import APIVersion + + resolved_base_id = None + if self.api_version == APIVersion.V3: + resolved_base_id = self._resolve_base_id(table_id, base_id) + + endpoint = self._path_builder.table_get_meta(table_id, resolved_base_id) + result = self._get(endpoint) return result if isinstance(result, dict) else {"data": result} def create_table(self, base_id: str, table_data: dict[str, Any]) -> dict[str, Any]: """Create a new table in a base. + Supports both API v2 and v3. + Args: base_id: The base ID where table will be created table_data: Table creation data (title, columns, etc.) @@ -359,15 +406,21 @@ def create_table(self, base_id: str, table_data: dict[str, Any]) -> dict[str, An ... } >>> table = meta_client.create_table("base123", table_data) """ - result = self._post(f"api/v2/meta/bases/{base_id}/tables", data=table_data) + endpoint = self._path_builder.tables_list_meta(base_id) + result = self._post(endpoint, data=table_data) return result if isinstance(result, dict) else {"data": result} - def update_table(self, table_id: str, table_data: dict[str, Any]) -> dict[str, Any]: + def update_table( + self, table_id: str, table_data: dict[str, Any], base_id: str | None = None + ) -> dict[str, Any]: """Update table metadata (title, description, etc.). + Supports both API v2 and v3. + Args: table_id: The table ID to update table_data: Updated table data + base_id: Base ID (required for v3, optional for v2) Returns: Updated table metadata @@ -376,16 +429,27 @@ def update_table(self, table_id: str, table_data: dict[str, Any]) -> dict[str, A NocoDBException: For API errors TableNotFoundException: If table is not found """ - result = self._patch(f"api/v2/meta/tables/{table_id}", data=table_data) + # Resolve base_id for v3 + from .api_version import APIVersion + + resolved_base_id = None + if self.api_version == APIVersion.V3: + resolved_base_id = self._resolve_base_id(table_id, base_id) + + endpoint = self._path_builder.table_get_meta(table_id, resolved_base_id) + result = self._patch(endpoint, data=table_data) return result if isinstance(result, dict) else {"data": result} - def delete_table(self, table_id: str) -> dict[str, Any]: + def delete_table(self, table_id: str, base_id: str | None = None) -> dict[str, Any]: """Delete a table and all its data. WARNING: This operation cannot be undone. All data in the table will be lost. + Supports both API v2 and v3. + Args: table_id: The table ID to delete + base_id: Base ID (required for v3, optional for v2) Returns: Deletion confirmation response @@ -394,18 +458,29 @@ def delete_table(self, table_id: str) -> dict[str, Any]: NocoDBException: For API errors TableNotFoundException: If table is not found """ - result = self._delete(f"api/v2/meta/tables/{table_id}") + # Resolve base_id for v3 + from .api_version import APIVersion + + resolved_base_id = None + if self.api_version == APIVersion.V3: + resolved_base_id = self._resolve_base_id(table_id, base_id) + + endpoint = self._path_builder.table_get_meta(table_id, resolved_base_id) + result = self._delete(endpoint) return result if isinstance(result, dict) else {"data": result} # ======================================================================== # COLUMN OPERATIONS (Meta API) # ======================================================================== - def list_columns(self, table_id: str) -> list[dict[str, Any]]: + def list_columns(self, table_id: str, base_id: str | None = None) -> list[dict[str, Any]]: """List all columns in a table. + Supports both API v2 and v3. Note: In v3, columns are called 'fields'. + Args: table_id: The table ID + base_id: Base ID (required for v3, optional for v2) Returns: List of column metadata including types, constraints, relationships @@ -414,16 +489,29 @@ def list_columns(self, table_id: str) -> list[dict[str, Any]]: NocoDBException: For API errors TableNotFoundException: If table is not found """ - response = self._get(f"api/v2/meta/tables/{table_id}/columns") + # Resolve base_id for v3 + from .api_version import APIVersion + + resolved_base_id = None + if self.api_version == APIVersion.V3: + resolved_base_id = self._resolve_base_id(table_id, base_id) + + endpoint = self._path_builder.columns_create(table_id, resolved_base_id) + response = self._get(endpoint) column_list = response.get("list", []) return column_list if isinstance(column_list, list) else [] - def create_column(self, table_id: str, column_data: dict[str, Any]) -> dict[str, Any]: + def create_column( + self, table_id: str, column_data: dict[str, Any], base_id: str | None = None + ) -> dict[str, Any]: """Create a new column in a table. + Supports both API v2 and v3. Note: In v3, columns are called 'fields'. + Args: table_id: The table ID where column will be created column_data: Column definition (title, type, constraints, etc.) + base_id: Base ID (required for v3, optional for v2) Returns: Created column metadata @@ -441,15 +529,28 @@ def create_column(self, table_id: str, column_data: dict[str, Any]) -> dict[str, ... } >>> column = meta_client.create_column("table123", column_data) """ - result = self._post(f"api/v2/meta/tables/{table_id}/columns", data=column_data) + # Resolve base_id for v3 + from .api_version import APIVersion + + resolved_base_id = None + if self.api_version == APIVersion.V3: + resolved_base_id = self._resolve_base_id(table_id, base_id) + + endpoint = self._path_builder.columns_create(table_id, resolved_base_id) + result = self._post(endpoint, data=column_data) return result if isinstance(result, dict) else {"data": result} - def update_column(self, column_id: str, column_data: dict[str, Any]) -> dict[str, Any]: + def update_column( + self, column_id: str, column_data: dict[str, Any], base_id: str | None = None + ) -> dict[str, Any]: """Update an existing column's properties. + Supports both API v2 and v3. Note: In v3, columns are called 'fields'. + Args: column_id: The column ID to update column_data: Updated column data (title, constraints, etc.) + base_id: Base ID (required for v3, optional for v2) Returns: Updated column metadata @@ -458,16 +559,29 @@ def update_column(self, column_id: str, column_data: dict[str, Any]) -> dict[str NocoDBException: For API errors ValidationException: If column_data is invalid """ - result = self._patch(f"api/v2/meta/columns/{column_id}", data=column_data) + # Resolve base_id for v3 (column_id doesn't directly resolve, so base_id must be provided) + from .api_version import APIVersion + + resolved_base_id = None + if self.api_version == APIVersion.V3: + if not base_id and not self.base_id: + raise ValueError("base_id is required for API v3 column operations") + resolved_base_id = base_id or self.base_id + + endpoint = self._path_builder.column_get(column_id, resolved_base_id) + result = self._patch(endpoint, data=column_data) return result if isinstance(result, dict) else {"data": result} - def delete_column(self, column_id: str) -> dict[str, Any]: + def delete_column(self, column_id: str, base_id: str | None = None) -> dict[str, Any]: """Delete a column from a table. WARNING: This will permanently delete the column and all its data. + Supports both API v2 and v3. Note: In v3, columns are called 'fields'. + Args: column_id: The column ID to delete + base_id: Base ID (required for v3, optional for v2) Returns: Deletion confirmation response @@ -475,18 +589,31 @@ def delete_column(self, column_id: str) -> dict[str, Any]: Raises: NocoDBException: For API errors """ - result = self._delete(f"api/v2/meta/columns/{column_id}") + # Resolve base_id for v3 (column_id doesn't directly resolve, so base_id must be provided) + from .api_version import APIVersion + + resolved_base_id = None + if self.api_version == APIVersion.V3: + if not base_id and not self.base_id: + raise ValueError("base_id is required for API v3 column operations") + resolved_base_id = base_id or self.base_id + + endpoint = self._path_builder.column_get(column_id, resolved_base_id) + result = self._delete(endpoint) return result if isinstance(result, dict) else {"data": result} # ======================================================================== # VIEW OPERATIONS (Meta API) # ======================================================================== - def list_views(self, table_id: str) -> list[dict[str, Any]]: + def list_views(self, table_id: str, base_id: str | None = None) -> list[dict[str, Any]]: """List all views for a table. + Supports both API v2 and v3. + Args: table_id: The table ID + base_id: Base ID (required for v3, optional for v2) Returns: List of view metadata (grid, gallery, form, kanban, calendar views) @@ -495,15 +622,26 @@ def list_views(self, table_id: str) -> list[dict[str, Any]]: NocoDBException: For API errors TableNotFoundException: If table is not found """ - response = self._get(f"api/v2/meta/tables/{table_id}/views") + # Resolve base_id for v3 + from .api_version import APIVersion + + resolved_base_id = None + if self.api_version == APIVersion.V3: + resolved_base_id = self._resolve_base_id(table_id, base_id) + + endpoint = self._path_builder.views_list(table_id, resolved_base_id) + response = self._get(endpoint) view_list = response.get("list", []) return view_list if isinstance(view_list, list) else [] - def get_view(self, view_id: str) -> dict[str, Any]: + def get_view(self, view_id: str, base_id: str | None = None) -> dict[str, Any]: """Get detailed view metadata. + Supports both API v2 and v3. + Args: view_id: The view ID + base_id: Base ID (required for v3, optional for v2) Returns: View metadata including filters, sorts, column configuration @@ -511,14 +649,29 @@ def get_view(self, view_id: str) -> dict[str, Any]: Raises: NocoDBException: For API errors """ - return self._get(f"api/v2/meta/views/{view_id}") + # Resolve base_id for v3 (view_id doesn't directly resolve, so base_id must be provided) + from .api_version import APIVersion + + resolved_base_id = None + if self.api_version == APIVersion.V3: + if not base_id and not self.base_id: + raise ValueError("base_id is required for API v3 view operations") + resolved_base_id = base_id or self.base_id - def create_view(self, table_id: str, view_data: dict[str, Any]) -> dict[str, Any]: + endpoint = self._path_builder.view_get(view_id, resolved_base_id) + return self._get(endpoint) + + def create_view( + self, table_id: str, view_data: dict[str, Any], base_id: str | None = None + ) -> dict[str, Any]: """Create a new view for a table. + Supports both API v2 and v3. + Args: table_id: The table ID where view will be created view_data: View configuration (title, type, filters, sorts) + base_id: Base ID (required for v3, optional for v2) Returns: Created view metadata @@ -535,15 +688,28 @@ def create_view(self, table_id: str, view_data: dict[str, Any]) -> dict[str, Any ... } >>> view = meta_client.create_view("table123", view_data) """ - result = self._post(f"api/v2/meta/tables/{table_id}/views", data=view_data) + # Resolve base_id for v3 + from .api_version import APIVersion + + resolved_base_id = None + if self.api_version == APIVersion.V3: + resolved_base_id = self._resolve_base_id(table_id, base_id) + + endpoint = self._path_builder.views_list(table_id, resolved_base_id) + result = self._post(endpoint, data=view_data) return result if isinstance(result, dict) else {"data": result} - def update_view(self, view_id: str, view_data: dict[str, Any]) -> dict[str, Any]: + def update_view( + self, view_id: str, view_data: dict[str, Any], base_id: str | None = None + ) -> dict[str, Any]: """Update view properties (title, filters, sorts, etc.). + Supports both API v2 and v3. + Args: view_id: The view ID to update view_data: Updated view configuration + base_id: Base ID (required for v3, optional for v2) Returns: Updated view metadata @@ -551,14 +717,27 @@ def update_view(self, view_id: str, view_data: dict[str, Any]) -> dict[str, Any] Raises: NocoDBException: For API errors """ - result = self._patch(f"api/v2/meta/views/{view_id}", data=view_data) + # Resolve base_id for v3 (view_id doesn't directly resolve, so base_id must be provided) + from .api_version import APIVersion + + resolved_base_id = None + if self.api_version == APIVersion.V3: + if not base_id and not self.base_id: + raise ValueError("base_id is required for API v3 view operations") + resolved_base_id = base_id or self.base_id + + endpoint = self._path_builder.view_get(view_id, resolved_base_id) + result = self._patch(endpoint, data=view_data) return result if isinstance(result, dict) else {"data": result} - def delete_view(self, view_id: str) -> dict[str, Any]: + def delete_view(self, view_id: str, base_id: str | None = None) -> dict[str, Any]: """Delete a view. + Supports both API v2 and v3. + Args: view_id: The view ID to delete + base_id: Base ID (required for v3, optional for v2) Returns: Deletion confirmation response @@ -566,18 +745,31 @@ def delete_view(self, view_id: str) -> dict[str, Any]: Raises: NocoDBException: For API errors """ - result = self._delete(f"api/v2/meta/views/{view_id}") + # Resolve base_id for v3 (view_id doesn't directly resolve, so base_id must be provided) + from .api_version import APIVersion + + resolved_base_id = None + if self.api_version == APIVersion.V3: + if not base_id and not self.base_id: + raise ValueError("base_id is required for API v3 view operations") + resolved_base_id = base_id or self.base_id + + endpoint = self._path_builder.view_get(view_id, resolved_base_id) + result = self._delete(endpoint) return result if isinstance(result, dict) else {"data": result} # ======================================================================== # WEBHOOK OPERATIONS (Meta API) # ======================================================================== - def list_webhooks(self, table_id: str) -> list[dict[str, Any]]: + def list_webhooks(self, table_id: str, base_id: str | None = None) -> list[dict[str, Any]]: """List all webhooks configured for a table. + Supports both API v2 and v3. + Args: table_id: The table ID + base_id: Base ID (required for v3, optional for v2) Returns: List of webhook configurations @@ -586,15 +778,26 @@ def list_webhooks(self, table_id: str) -> list[dict[str, Any]]: NocoDBException: For API errors TableNotFoundException: If table is not found """ - response = self._get(f"api/v2/meta/tables/{table_id}/hooks") + # Resolve base_id for v3 + from .api_version import APIVersion + + resolved_base_id = None + if self.api_version == APIVersion.V3: + resolved_base_id = self._resolve_base_id(table_id, base_id) + + endpoint = self._path_builder.webhooks_list(table_id, resolved_base_id) + response = self._get(endpoint) webhook_list = response.get("list", []) return webhook_list if isinstance(webhook_list, list) else [] - def get_webhook(self, hook_id: str) -> dict[str, Any]: + def get_webhook(self, hook_id: str, base_id: str | None = None) -> dict[str, Any]: """Get webhook configuration details. + Supports both API v2 and v3. + Args: hook_id: The webhook ID + base_id: Base ID (required for v3, optional for v2) Returns: Webhook configuration including URL, events, conditions @@ -602,14 +805,29 @@ def get_webhook(self, hook_id: str) -> dict[str, Any]: Raises: NocoDBException: For API errors """ - return self._get(f"api/v2/meta/hooks/{hook_id}") + # Resolve base_id for v3 (hook_id doesn't directly resolve, so base_id must be provided) + from .api_version import APIVersion - def create_webhook(self, table_id: str, webhook_data: dict[str, Any]) -> dict[str, Any]: + resolved_base_id = None + if self.api_version == APIVersion.V3: + if not base_id and not self.base_id: + raise ValueError("base_id is required for API v3 webhook operations") + resolved_base_id = base_id or self.base_id + + endpoint = self._path_builder.webhook_get(hook_id, resolved_base_id) + return self._get(endpoint) + + def create_webhook( + self, table_id: str, webhook_data: dict[str, Any], base_id: str | None = None + ) -> dict[str, Any]: """Create a new webhook for table events. + Supports both API v2 and v3. + Args: table_id: The table ID where webhook will be created webhook_data: Webhook configuration (URL, events, conditions) + base_id: Base ID (required for v3, optional for v2) Returns: Created webhook configuration @@ -635,15 +853,28 @@ def create_webhook(self, table_id: str, webhook_data: dict[str, Any]) -> dict[st ... } >>> webhook = meta_client.create_webhook("table123", webhook_data) """ - result = self._post(f"api/v2/meta/tables/{table_id}/hooks", data=webhook_data) + # Resolve base_id for v3 + from .api_version import APIVersion + + resolved_base_id = None + if self.api_version == APIVersion.V3: + resolved_base_id = self._resolve_base_id(table_id, base_id) + + endpoint = self._path_builder.webhooks_list(table_id, resolved_base_id) + result = self._post(endpoint, data=webhook_data) return result if isinstance(result, dict) else {"data": result} - def update_webhook(self, hook_id: str, webhook_data: dict[str, Any]) -> dict[str, Any]: + def update_webhook( + self, hook_id: str, webhook_data: dict[str, Any], base_id: str | None = None + ) -> dict[str, Any]: """Update webhook configuration. + Supports both API v2 and v3. + Args: hook_id: The webhook ID to update webhook_data: Updated webhook configuration + base_id: Base ID (required for v3, optional for v2) Returns: Updated webhook configuration @@ -651,14 +882,27 @@ def update_webhook(self, hook_id: str, webhook_data: dict[str, Any]) -> dict[str Raises: NocoDBException: For API errors """ - result = self._patch(f"api/v2/meta/hooks/{hook_id}", data=webhook_data) + # Resolve base_id for v3 (hook_id doesn't directly resolve, so base_id must be provided) + from .api_version import APIVersion + + resolved_base_id = None + if self.api_version == APIVersion.V3: + if not base_id and not self.base_id: + raise ValueError("base_id is required for API v3 webhook operations") + resolved_base_id = base_id or self.base_id + + endpoint = self._path_builder.webhook_get(hook_id, resolved_base_id) + result = self._patch(endpoint, data=webhook_data) return result if isinstance(result, dict) else {"data": result} - def delete_webhook(self, hook_id: str) -> dict[str, Any]: + def delete_webhook(self, hook_id: str, base_id: str | None = None) -> dict[str, Any]: """Delete a webhook. + Supports both API v2 and v3. + Args: hook_id: The webhook ID to delete + base_id: Base ID (required for v3, optional for v2) Returns: Deletion confirmation response @@ -666,14 +910,27 @@ def delete_webhook(self, hook_id: str) -> dict[str, Any]: Raises: NocoDBException: For API errors """ - result = self._delete(f"api/v2/meta/hooks/{hook_id}") + # Resolve base_id for v3 (hook_id doesn't directly resolve, so base_id must be provided) + from .api_version import APIVersion + + resolved_base_id = None + if self.api_version == APIVersion.V3: + if not base_id and not self.base_id: + raise ValueError("base_id is required for API v3 webhook operations") + resolved_base_id = base_id or self.base_id + + endpoint = self._path_builder.webhook_get(hook_id, resolved_base_id) + result = self._delete(endpoint) return result if isinstance(result, dict) else {"data": result} - def test_webhook(self, hook_id: str) -> dict[str, Any]: + def test_webhook(self, hook_id: str, base_id: str | None = None) -> dict[str, Any]: """Test a webhook by triggering it manually. + Supports both API v2 and v3. + Args: hook_id: The webhook ID to test + base_id: Base ID (required for v3, optional for v2) Returns: Test execution results including HTTP response details @@ -681,5 +938,16 @@ def test_webhook(self, hook_id: str) -> dict[str, Any]: Raises: NocoDBException: For API errors """ - result = self._post(f"api/v2/meta/hooks/{hook_id}/test", data={}) + # Resolve base_id for v3 (hook_id doesn't directly resolve, so base_id must be provided) + from .api_version import APIVersion + + resolved_base_id = None + if self.api_version == APIVersion.V3: + if not base_id and not self.base_id: + raise ValueError("base_id is required for API v3 webhook operations") + resolved_base_id = base_id or self.base_id + + # Build endpoint for webhook test + endpoint = self._path_builder.webhook_get(hook_id, resolved_base_id) + "/test" + result = self._post(endpoint, data={}) return result if isinstance(result, dict) else {"data": result} diff --git a/tests/test_api_version.py b/tests/test_api_version.py new file mode 100644 index 0000000..71bd1e2 --- /dev/null +++ b/tests/test_api_version.py @@ -0,0 +1,407 @@ +"""Tests for API version support (PathBuilder, QueryParamAdapter). + +MIT License + +Copyright (c) BAUER GROUP +""" + +import pytest + +from nocodb_simple_client.api_version import APIVersion, PathBuilder, QueryParamAdapter + + +class TestAPIVersion: + """Test APIVersion enum.""" + + def test_api_version_v2(self): + """Test v2 API version.""" + assert APIVersion.V2 == "v2" + assert str(APIVersion.V2) == "v2" + + def test_api_version_v3(self): + """Test v3 API version.""" + assert APIVersion.V3 == "v3" + assert str(APIVersion.V3) == "v3" + + def test_api_version_creation(self): + """Test creating APIVersion from string.""" + assert APIVersion("v2") == APIVersion.V2 + assert APIVersion("v3") == APIVersion.V3 + + +class TestQueryParamAdapter: + """Test QueryParamAdapter for parameter conversion.""" + + def test_convert_pagination_to_v3_basic(self): + """Test basic offset/limit to page/pageSize conversion.""" + params = {"offset": 50, "limit": 25} + result = QueryParamAdapter.convert_pagination_to_v3(params) + + assert result["page"] == 3 + assert result["pageSize"] == 25 + assert "offset" not in result + assert "limit" not in result + + def test_convert_pagination_to_v3_first_page(self): + """Test conversion for first page.""" + params = {"offset": 0, "limit": 10} + result = QueryParamAdapter.convert_pagination_to_v3(params) + + assert result["page"] == 1 + assert result["pageSize"] == 10 + + def test_convert_pagination_to_v3_no_params(self): + """Test conversion with no pagination params.""" + params = {"where": "(Status,eq,Active)"} + result = QueryParamAdapter.convert_pagination_to_v3(params) + + assert "page" not in result + assert "pageSize" not in result + assert result["where"] == "(Status,eq,Active)" + + def test_convert_pagination_to_v3_only_limit(self): + """Test conversion with only limit.""" + params = {"limit": 20} + result = QueryParamAdapter.convert_pagination_to_v3(params) + + assert result["page"] == 1 + assert result["pageSize"] == 20 + + def test_convert_pagination_to_v2_basic(self): + """Test basic page/pageSize to offset/limit conversion.""" + params = {"page": 3, "pageSize": 25} + result = QueryParamAdapter.convert_pagination_to_v2(params) + + assert result["offset"] == 50 + assert result["limit"] == 25 + assert "page" not in result + assert "pageSize" not in result + + def test_convert_pagination_to_v2_first_page(self): + """Test conversion for first page.""" + params = {"page": 1, "pageSize": 10} + result = QueryParamAdapter.convert_pagination_to_v2(params) + + assert result["offset"] == 0 + assert result["limit"] == 10 + + def test_convert_sort_to_v3_single_field_asc(self): + """Test sort conversion for single ascending field.""" + result = QueryParamAdapter.convert_sort_to_v3("name") + + assert len(result) == 1 + assert result[0]["field"] == "name" + assert result[0]["direction"] == "asc" + + def test_convert_sort_to_v3_single_field_desc(self): + """Test sort conversion for single descending field.""" + result = QueryParamAdapter.convert_sort_to_v3("-age") + + assert len(result) == 1 + assert result[0]["field"] == "age" + assert result[0]["direction"] == "desc" + + def test_convert_sort_to_v3_multiple_fields(self): + """Test sort conversion for multiple fields.""" + result = QueryParamAdapter.convert_sort_to_v3("name,-age,email") + + assert len(result) == 3 + assert result[0] == {"field": "name", "direction": "asc"} + assert result[1] == {"field": "age", "direction": "desc"} + assert result[2] == {"field": "email", "direction": "asc"} + + def test_convert_sort_to_v3_none(self): + """Test sort conversion with None.""" + result = QueryParamAdapter.convert_sort_to_v3(None) + assert result is None + + def test_convert_sort_to_v3_empty_string(self): + """Test sort conversion with empty string.""" + result = QueryParamAdapter.convert_sort_to_v3("") + assert result is None + + def test_convert_sort_to_v2_single_field_asc(self): + """Test reverse sort conversion for ascending field.""" + sort_list = [{"field": "name", "direction": "asc"}] + result = QueryParamAdapter.convert_sort_to_v2(sort_list) + + assert result == "name" + + def test_convert_sort_to_v2_single_field_desc(self): + """Test reverse sort conversion for descending field.""" + sort_list = [{"field": "age", "direction": "desc"}] + result = QueryParamAdapter.convert_sort_to_v2(sort_list) + + assert result == "-age" + + def test_convert_sort_to_v2_multiple_fields(self): + """Test reverse sort conversion for multiple fields.""" + sort_list = [ + {"field": "name", "direction": "asc"}, + {"field": "age", "direction": "desc"}, + {"field": "email", "direction": "asc"}, + ] + result = QueryParamAdapter.convert_sort_to_v2(sort_list) + + assert result == "name,-age,email" + + def test_convert_sort_to_v2_none(self): + """Test reverse sort conversion with None.""" + result = QueryParamAdapter.convert_sort_to_v2(None) + assert result is None + + def test_convert_where_operators_to_v3(self): + """Test where clause operator conversion to v3.""" + where = {"field": {"ne": "value"}} + result = QueryParamAdapter.convert_where_operators_to_v3(where) + + assert "neq" in result["field"] + assert "ne" not in result["field"] + assert result["field"]["neq"] == "value" + + def test_convert_where_operators_to_v3_nested(self): + """Test nested where clause conversion.""" + where = {"and": [{"field1": {"ne": "val1"}}, {"field2": {"ne": "val2"}}]} + result = QueryParamAdapter.convert_where_operators_to_v3(where) + + assert result["and"][0]["field1"]["neq"] == "val1" + assert result["and"][1]["field2"]["neq"] == "val2" + + def test_convert_where_operators_to_v3_none(self): + """Test where conversion with None.""" + result = QueryParamAdapter.convert_where_operators_to_v3(None) + assert result is None + + def test_convert_where_operators_to_v2(self): + """Test where clause operator conversion to v2.""" + where = {"field": {"neq": "value"}} + result = QueryParamAdapter.convert_where_operators_to_v2(where) + + assert "ne" in result["field"] + assert "neq" not in result["field"] + assert result["field"]["ne"] == "value" + + +class TestPathBuilderDataAPI: + """Test PathBuilder for Data API endpoints.""" + + def test_records_list_v2(self): + """Test records list path for v2.""" + builder = PathBuilder(APIVersion.V2) + path = builder.records_list("table_123") + + assert path == "api/v2/tables/table_123/records" + + def test_records_list_v3(self): + """Test records list path for v3.""" + builder = PathBuilder(APIVersion.V3) + path = builder.records_list("table_123", "base_abc") + + assert path == "api/v3/data/base_abc/table_123/records" + + def test_records_list_v3_no_base_id(self): + """Test v3 records list requires base_id.""" + builder = PathBuilder(APIVersion.V3) + + with pytest.raises(ValueError, match="base_id is required"): + builder.records_list("table_123") + + def test_records_get_v2(self): + """Test get record path for v2.""" + builder = PathBuilder(APIVersion.V2) + path = builder.records_get("table_123", "rec_456") + + assert path == "api/v2/tables/table_123/records/rec_456" + + def test_records_get_v3(self): + """Test get record path for v3.""" + builder = PathBuilder(APIVersion.V3) + path = builder.records_get("table_123", "rec_456", "base_abc") + + assert path == "api/v3/data/base_abc/table_123/records/rec_456" + + def test_records_count_v2(self): + """Test count records path for v2.""" + builder = PathBuilder(APIVersion.V2) + path = builder.records_count("table_123") + + assert path == "api/v2/tables/table_123/records/count" + + def test_records_count_v3(self): + """Test count records path for v3.""" + builder = PathBuilder(APIVersion.V3) + path = builder.records_count("table_123", "base_abc") + + assert path == "api/v3/data/base_abc/table_123/count" + + def test_links_list_v2(self): + """Test links list path for v2.""" + builder = PathBuilder(APIVersion.V2) + path = builder.links_list("table_123", "link_field_456", "rec_789") + + assert path == "api/v2/tables/table_123/links/link_field_456/records/rec_789" + + def test_links_list_v3(self): + """Test links list path for v3.""" + builder = PathBuilder(APIVersion.V3) + path = builder.links_list("table_123", "link_field_456", "rec_789", "base_abc") + + assert path == "api/v3/data/base_abc/table_123/links/link_field_456/rec_789" + + def test_file_upload_v2(self): + """Test file upload path for v2.""" + builder = PathBuilder(APIVersion.V2) + path = builder.file_upload("table_123") + + assert path == "api/v2/tables/table_123/attachments" + + def test_file_upload_v3(self): + """Test file upload path for v3.""" + builder = PathBuilder(APIVersion.V3) + path = builder.file_upload("table_123", "base_abc") + + assert path == "api/v3/data/base_abc/table_123/attachments" + + +class TestPathBuilderMetaAPI: + """Test PathBuilder for Meta API endpoints.""" + + def test_bases_list_v2(self): + """Test bases list path for v2.""" + builder = PathBuilder(APIVersion.V2) + path = builder.bases_list() + + assert path == "api/v2/meta/bases" + + def test_bases_list_v3(self): + """Test bases list path for v3.""" + builder = PathBuilder(APIVersion.V3) + path = builder.bases_list() + + assert path == "api/v3/meta/bases" + + def test_base_get_v2(self): + """Test get base path for v2.""" + builder = PathBuilder(APIVersion.V2) + path = builder.base_get("base_123") + + assert path == "api/v2/meta/bases/base_123" + + def test_base_get_v3(self): + """Test get base path for v3.""" + builder = PathBuilder(APIVersion.V3) + path = builder.base_get("base_123") + + assert path == "api/v3/meta/bases/base_123" + + def test_tables_list_meta_v2(self): + """Test tables list path for v2.""" + builder = PathBuilder(APIVersion.V2) + path = builder.tables_list_meta("base_123") + + assert path == "api/v2/meta/bases/base_123/tables" + + def test_tables_list_meta_v3(self): + """Test tables list path for v3.""" + builder = PathBuilder(APIVersion.V3) + path = builder.tables_list_meta("base_123") + + assert path == "api/v3/meta/bases/base_123/tables" + + def test_table_get_meta_v2(self): + """Test get table metadata path for v2.""" + builder = PathBuilder(APIVersion.V2) + path = builder.table_get_meta("table_123") + + assert path == "api/v2/meta/tables/table_123" + + def test_table_get_meta_v3(self): + """Test get table metadata path for v3.""" + builder = PathBuilder(APIVersion.V3) + path = builder.table_get_meta("table_123", "base_abc") + + assert path == "api/v3/meta/bases/base_abc/tables/table_123" + + def test_column_get_v2(self): + """Test get column path for v2.""" + builder = PathBuilder(APIVersion.V2) + path = builder.column_get("col_123") + + assert path == "api/v2/meta/columns/col_123" + + def test_column_get_v3(self): + """Test get field path for v3 (columns → fields).""" + builder = PathBuilder(APIVersion.V3) + path = builder.column_get("field_123", "base_abc") + + assert path == "api/v3/meta/bases/base_abc/fields/field_123" + + def test_columns_create_v2(self): + """Test create column path for v2.""" + builder = PathBuilder(APIVersion.V2) + path = builder.columns_create("table_123") + + assert path == "api/v2/meta/tables/table_123/columns" + + def test_columns_create_v3(self): + """Test create field path for v3 (columns → fields).""" + builder = PathBuilder(APIVersion.V3) + path = builder.columns_create("table_123", "base_abc") + + assert path == "api/v3/meta/bases/base_abc/tables/table_123/fields" + + def test_view_get_v2(self): + """Test get view path for v2.""" + builder = PathBuilder(APIVersion.V2) + path = builder.view_get("view_123") + + assert path == "api/v2/meta/views/view_123" + + def test_view_get_v3(self): + """Test get view path for v3.""" + builder = PathBuilder(APIVersion.V3) + path = builder.view_get("view_123", "base_abc") + + assert path == "api/v3/meta/bases/base_abc/views/view_123" + + def test_views_list_v2(self): + """Test list views path for v2.""" + builder = PathBuilder(APIVersion.V2) + path = builder.views_list("table_123") + + assert path == "api/v2/meta/tables/table_123/views" + + def test_views_list_v3(self): + """Test list views path for v3.""" + builder = PathBuilder(APIVersion.V3) + path = builder.views_list("table_123", "base_abc") + + assert path == "api/v3/meta/bases/base_abc/tables/table_123/views" + + def test_webhook_get_v2(self): + """Test get webhook path for v2.""" + builder = PathBuilder(APIVersion.V2) + path = builder.webhook_get("hook_123") + + assert path == "api/v2/meta/hooks/hook_123" + + def test_webhook_get_v3(self): + """Test get webhook path for v3.""" + builder = PathBuilder(APIVersion.V3) + path = builder.webhook_get("hook_123", "base_abc") + + assert path == "api/v3/meta/bases/base_abc/hooks/hook_123" + + def test_webhooks_list_v2(self): + """Test list webhooks path for v2.""" + builder = PathBuilder(APIVersion.V2) + path = builder.webhooks_list("table_123") + + assert path == "api/v2/meta/tables/table_123/hooks" + + def test_webhooks_list_v3(self): + """Test list webhooks path for v3.""" + builder = PathBuilder(APIVersion.V3) + path = builder.webhooks_list("table_123", "base_abc") + + assert path == "api/v3/meta/bases/base_abc/tables/table_123/hooks" diff --git a/tests/test_client.py b/tests/test_client.py index ccc5502..68ecc05 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -280,7 +280,7 @@ def test_attach_file_to_record(self, client, mock_session): result = client.attach_file_to_record("test-table", 123, "Document", "/path/file.txt") assert result == 123 - mock_upload.assert_called_once_with("test-table", "/path/file.txt") + mock_upload.assert_called_once_with("test-table", "/path/file.txt", None) def test_download_file_from_record(self, client, mock_session): """Test downloading file from record.""" diff --git a/tests/test_meta_client.py b/tests/test_meta_client.py index 1278f2c..97b1525 100644 --- a/tests/test_meta_client.py +++ b/tests/test_meta_client.py @@ -6,6 +6,31 @@ from nocodb_simple_client.meta_client import NocoDBMetaClient from nocodb_simple_client.client import NocoDBClient from nocodb_simple_client.config import NocoDBConfig +from nocodb_simple_client.api_version import APIVersion, PathBuilder + + +def setup_meta_client_mock(client_mock): + """Setup mock client with PathBuilder and API version for v2.""" + # Mock API version + client_mock.api_version = APIVersion.V2 + + # Mock PathBuilder + path_builder_mock = Mock(spec=PathBuilder) + client_mock._path_builder = path_builder_mock + + # Setup PathBuilder methods to return v2 endpoints + path_builder_mock.bases_list.return_value = "api/v2/meta/bases" + path_builder_mock.base_get.side_effect = lambda base_id: f"api/v2/meta/bases/{base_id}" + path_builder_mock.tables_list_meta.side_effect = lambda base_id: f"api/v2/meta/bases/{base_id}/tables" + path_builder_mock.table_get_meta.side_effect = lambda table_id, base_id=None: f"api/v2/meta/tables/{table_id}" + path_builder_mock.columns_create.side_effect = lambda table_id, base_id=None: f"api/v2/meta/tables/{table_id}/columns" + path_builder_mock.column_get.side_effect = lambda column_id, base_id=None: f"api/v2/meta/columns/{column_id}" + path_builder_mock.views_list.side_effect = lambda table_id, base_id=None: f"api/v2/meta/tables/{table_id}/views" + path_builder_mock.view_get.side_effect = lambda view_id, base_id=None: f"api/v2/meta/views/{view_id}" + path_builder_mock.webhooks_list.side_effect = lambda table_id, base_id=None: f"api/v2/meta/tables/{table_id}/hooks" + path_builder_mock.webhook_get.side_effect = lambda hook_id, base_id=None: f"api/v2/meta/hooks/{hook_id}" + + return client_mock class TestMetaClientInheritance: @@ -48,6 +73,7 @@ class TestTableOperations: def meta_client(self): """Create meta client with mocked HTTP methods.""" client = Mock(spec=NocoDBMetaClient) + setup_meta_client_mock(client) # Make sure it has the required methods client.list_tables = NocoDBMetaClient.list_tables.__get__(client) client.get_table_info = NocoDBMetaClient.get_table_info.__get__(client) @@ -155,6 +181,7 @@ class TestWorkspaceOperations: def meta_client(self): """Create meta client with mocked HTTP methods.""" client = Mock(spec=NocoDBMetaClient) + setup_meta_client_mock(client) client.list_workspaces = NocoDBMetaClient.list_workspaces.__get__(client) client.get_workspace = NocoDBMetaClient.get_workspace.__get__(client) client.create_workspace = NocoDBMetaClient.create_workspace.__get__(client) @@ -241,6 +268,7 @@ class TestBaseOperations: def meta_client(self): """Create meta client with mocked HTTP methods.""" client = Mock(spec=NocoDBMetaClient) + setup_meta_client_mock(client) client.list_bases = NocoDBMetaClient.list_bases.__get__(client) client.get_base = NocoDBMetaClient.get_base.__get__(client) client.create_base = NocoDBMetaClient.create_base.__get__(client) @@ -260,7 +288,7 @@ def test_list_bases(self, meta_client): result = meta_client.list_bases() assert result == expected_bases - meta_client._get.assert_called_once_with("api/v2/meta/bases/") + meta_client._get.assert_called_once_with("api/v2/meta/bases") def test_list_bases_empty_response(self, meta_client): """Test list_bases with empty response.""" @@ -327,6 +355,7 @@ class TestColumnOperations: def meta_client(self): """Create meta client with mocked HTTP methods.""" client = Mock(spec=NocoDBMetaClient) + setup_meta_client_mock(client) client.list_columns = NocoDBMetaClient.list_columns.__get__(client) client.create_column = NocoDBMetaClient.create_column.__get__(client) client.update_column = NocoDBMetaClient.update_column.__get__(client) @@ -400,6 +429,7 @@ class TestViewOperations: def meta_client(self): """Create meta client with mocked HTTP methods.""" client = Mock(spec=NocoDBMetaClient) + setup_meta_client_mock(client) client.list_views = NocoDBMetaClient.list_views.__get__(client) client.get_view = NocoDBMetaClient.get_view.__get__(client) client.create_view = NocoDBMetaClient.create_view.__get__(client) @@ -487,6 +517,7 @@ class TestWebhookOperations: def meta_client(self): """Create meta client with mocked HTTP methods.""" client = Mock(spec=NocoDBMetaClient) + setup_meta_client_mock(client) client.list_webhooks = NocoDBMetaClient.list_webhooks.__get__(client) client.get_webhook = NocoDBMetaClient.get_webhook.__get__(client) client.create_webhook = NocoDBMetaClient.create_webhook.__get__(client) @@ -599,6 +630,7 @@ class TestMetaClientEndpoints: def meta_client(self): """Create meta client with mocked HTTP methods.""" client = Mock(spec=NocoDBMetaClient) + setup_meta_client_mock(client) client.list_tables = NocoDBMetaClient.list_tables.__get__(client) client.get_table_info = NocoDBMetaClient.get_table_info.__get__(client) client.create_table = NocoDBMetaClient.create_table.__get__(client) @@ -628,6 +660,7 @@ class TestMetaClientErrorHandling: def meta_client(self): """Create meta client with mocked HTTP methods.""" client = Mock(spec=NocoDBMetaClient) + setup_meta_client_mock(client) client.list_tables = NocoDBMetaClient.list_tables.__get__(client) return client @@ -655,6 +688,7 @@ class TestMetaClientIntegration: def meta_client(self): """Create meta client for integration testing.""" client = Mock(spec=NocoDBMetaClient) + setup_meta_client_mock(client) client.list_tables = NocoDBMetaClient.list_tables.__get__(client) client.create_table = NocoDBMetaClient.create_table.__get__(client) client.delete_table = NocoDBMetaClient.delete_table.__get__(client) From d7c359fc19aac96d6515baad0cbdd7fa6ab97d5e Mon Sep 17 00:00:00 2001 From: Karl Bauer Date: Fri, 10 Oct 2025 14:38:02 +0200 Subject: [PATCH 5/7] feat: Implement comprehensive support for NocoDB API v2 and v3, including automatic parameter conversion and backward compatibility --- CHANGELOG.md | 33 +++ docs/README.template.MD | 59 ++++- src/nocodb_simple_client/table.py | 68 ++++-- tests/test_base_resolver.py | 227 ++++++++++++++++++ tests/test_table.py | 18 +- tests/test_version_switching.py | 368 ++++++++++++++++++++++++++++++ 6 files changed, 748 insertions(+), 25 deletions(-) create mode 100644 tests/test_base_resolver.py create mode 100644 tests/test_version_switching.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 21f2131..1a2e258 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,39 @@ +## Unreleased + +### Features + +- **API Version Support**: Add seamless support for NocoDB API v2 and v3 + - Implement `api_version` parameter for client initialization (default: "v2") + - Add automatic parameter conversion between v2 and v3 formats + - Pagination: `offset/limit` (v2) ↔ `page/pageSize` (v3) + - Sort: string format (v2) ↔ JSON array format (v3) + - Operators: automatic conversion (e.g., `ne` → `neq`) + - Implement `BaseIdResolver` with caching for v3 API base_id resolution + - Add v2/v3 support to all Data API methods (14 methods) + - Add v2/v3 support to all Meta API methods (29 methods) + - Update `NocoDBTable` wrapper to support `base_id` parameter + - Full backward compatibility maintained for existing v2 code + +### Documentation + +- Add comprehensive API Version Guide ([docs/API_VERSION_GUIDE.md](docs/API_VERSION_GUIDE.md)) +- Add Data API v2/v3 usage examples ([examples/api_version_example.py](examples/api_version_example.py)) +- Add Meta API v2/v3 usage examples ([examples/meta_api_version_example.py](examples/meta_api_version_example.py)) +- Update README.template.md with API version support documentation +- Add OpenAPI v2 and v3 specification files to docs directory + +### Tests + +- Add 88 new unit tests for v2/v3 functionality + - 52 tests for PathBuilder and QueryParamAdapter + - 15 tests for BaseIdResolver + - 21 integration tests for version switching +- Update existing tests for new `base_id` parameter +- All 454 tests passing with full coverage + ## v1.2.0 (2025-10-09) ### Bug Fixes diff --git a/docs/README.template.MD b/docs/README.template.MD index 66566d7..a4aac1e 100644 --- a/docs/README.template.MD +++ b/docs/README.template.MD @@ -24,12 +24,18 @@ A simple and powerful Python client for interacting with [NocoDB](https://nocodb - **Bulk Operations**: High-performance bulk insert, update, and delete operations - **File Management**: Upload, download, and manage file attachments - **Advanced Querying**: Complex filtering, sorting, and pagination support with SQL-like query builder +- **API Version Support**: Seamlessly switch between NocoDB API v2 and v3 + - Default v2 support with full backward compatibility + - Optional v3 support with automatic parameter conversion + - Automatic base_id resolution for v3 API + - Transparent pagination, sorting, and operator conversion - **Meta API Support**: Full access to NocoDB Meta API for structure management - Workspace, Base, and Table operations - Column/Field management with type-specific helpers - View management (Grid, Gallery, Form, Kanban, Calendar) - Webhook automation (URL, Email, Slack, Teams) - Link/Relation management + - Full v2/v3 API version support - **Flexible Configuration**: Multiple configuration methods (environment variables, files, direct parameters) - **Type Hints**: Full type annotation support for better IDE experience - **Error Handling**: Comprehensive exception handling with specific error types @@ -107,6 +113,44 @@ with NocoDBClient( # Client automatically closes when exiting the context ``` +### API Version Support (v2 & v3) + +The client supports both NocoDB API v2 (default) and v3 with automatic parameter conversion: + +```python +from nocodb_simple_client import NocoDBClient + +# Using v2 API (default - fully backward compatible) +client_v2 = NocoDBClient( + base_url="https://your-nocodb-instance.com", + db_auth_token="your-api-token" +) + +# Using v3 API with automatic base_id resolution +client_v3 = NocoDBClient( + base_url="https://your-nocodb-instance.com", + db_auth_token="your-api-token", + api_version="v3", + base_id="your-base-id" # Optional, can be auto-resolved +) + +# All methods work the same way - conversion is automatic! +records = client_v3.get_records( + table_id="your-table-id", + limit=25, # Automatically converts to page/pageSize for v3 + sort="-CreatedAt", # Automatically converts to JSON array for v3 + where="(Status,ne,Inactive)" # Operators converted automatically (ne -> neq) +) +``` + +**Key Features:** +- **Automatic Conversion**: Pagination (`offset/limit` ↔ `page/pageSize`), sort formats, and operators +- **Base ID Resolution**: Automatic table_id to base_id mapping with caching for v3 +- **Full Compatibility**: All Data API and Meta API methods support both versions +- **No Code Changes**: Existing v2 code works without modifications + +For detailed information, see the [API Version Guide](docs/API_VERSION_GUIDE.md) and [examples](examples/api_version_example.py). + ## 📚 Documentation ### Client Configuration @@ -117,6 +161,8 @@ The `NocoDBClient` supports various configuration options: client = NocoDBClient( base_url="https://your-nocodb-instance.com", db_auth_token="your-api-token", + api_version="v2", # API version: "v2" (default) or "v3" + base_id="your-base-id", # Base ID (optional, for v3 API) access_protection_auth="your-protection-token", # Value for protection header access_protection_header="X-Custom-Auth", # Custom header name (optional) max_redirects=3, # Maximum number of redirects @@ -465,18 +511,27 @@ The Meta API allows you to programmatically manage your NocoDB structure, includ ```python from nocodb_simple_client import NocoDBMetaClient, NocoDBConfig -# Initialize Meta API client +# Initialize Meta API client with v2 (default) meta_client = NocoDBMetaClient( base_url="https://app.nocodb.com", api_token="your-api-token" ) +# Initialize with v3 API +meta_client_v3 = NocoDBMetaClient( + base_url="https://app.nocodb.com", + api_token="your-api-token", + api_version="v3", + base_id="your-base-id" # Optional, can be auto-resolved +) + # Or with config config = NocoDBConfig(base_url="...", api_token="...") meta_client = NocoDBMetaClient(config) # Meta client inherits all data operations from NocoDBClient # So you can use it for both meta operations AND data operations +# Both v2 and v3 are fully supported! records = meta_client.get_records("table_id") # Data operation tables = meta_client.list_tables("base_id") # Meta operation ``` @@ -951,6 +1006,8 @@ except Exception as e: Check out the [`examples/`](examples/) directory for comprehensive examples: - **[Basic Usage](examples/basic_usage.py)**: CRUD operations and fundamentals +- **[API Version Support](examples/api_version_example.py)**: Using v2 and v3 APIs with automatic conversion +- **[Meta API Version Support](examples/meta_api_version_example.py)**: Meta API operations with v2/v3 - **[File Operations](examples/file_operations.py)**: File upload/download examples - **[Advanced Querying](examples/advanced_querying.py)**: Complex filtering and sorting with Query Builder - **[Context Manager Usage](examples/context_manager_usage.py)**: Proper resource management diff --git a/src/nocodb_simple_client/table.py b/src/nocodb_simple_client/table.py index 43a1623..73cc7a6 100644 --- a/src/nocodb_simple_client/table.py +++ b/src/nocodb_simple_client/table.py @@ -60,6 +60,7 @@ def get_records( where: str | None = None, fields: list[str] | None = None, limit: int = 25, + base_id: str | None = None, ) -> list[dict[str, Any]]: """Get multiple records from the table. @@ -68,6 +69,7 @@ def get_records( where: Filter condition (e.g., "(Name,eq,John)") fields: List of fields to retrieve limit: Maximum number of records to retrieve + base_id: Base ID (required for v3, optional for v2) Returns: List of record dictionaries @@ -76,18 +78,22 @@ def get_records( RecordNotFoundException: If no records match the criteria NocoDBException: For other API errors """ - return self.client.get_records(self.table_id, sort, where, fields, limit) + return self.client.get_records( + self.table_id, base_id=base_id, sort=sort, where=where, fields=fields, limit=limit + ) def get_record( self, record_id: int | str, fields: list[str] | None = None, + base_id: str | None = None, ) -> dict[str, Any]: """Get a single record by ID. Args: record_id: The ID of the record fields: List of fields to retrieve + base_id: Base ID (required for v3, optional for v2) Returns: Record dictionary @@ -96,13 +102,18 @@ def get_record( RecordNotFoundException: If the record is not found NocoDBException: For other API errors """ - return self.client.get_record(self.table_id, record_id, fields) + return self.client.get_record(self.table_id, record_id, fields, base_id=base_id) - def insert_record(self, record: dict[str, Any]) -> int | str: + def insert_record( + self, + record: dict[str, Any], + base_id: str | None = None, + ) -> int | str: """Insert a new record into the table. Args: record: Dictionary containing the record data + base_id: Base ID (required for v3, optional for v2) Returns: The ID of the inserted record @@ -110,18 +121,20 @@ def insert_record(self, record: dict[str, Any]) -> int | str: Raises: NocoDBException: For API errors """ - return self.client.insert_record(self.table_id, record) + return self.client.insert_record(self.table_id, record, base_id=base_id) def update_record( self, record: dict[str, Any], record_id: int | str | None = None, + base_id: str | None = None, ) -> int | str: """Update an existing record. Args: record: Dictionary containing the updated record data record_id: The ID of the record to update (optional if included in record) + base_id: Base ID (required for v3, optional for v2) Returns: The ID of the updated record @@ -130,13 +143,18 @@ def update_record( RecordNotFoundException: If the record is not found NocoDBException: For other API errors """ - return self.client.update_record(self.table_id, record, record_id) + return self.client.update_record(self.table_id, record, record_id, base_id=base_id) - def delete_record(self, record_id: int | str) -> int | str: + def delete_record( + self, + record_id: int | str, + base_id: str | None = None, + ) -> int | str: """Delete a record from the table. Args: record_id: The ID of the record to delete + base_id: Base ID (required for v3, optional for v2) Returns: The ID of the deleted record @@ -145,13 +163,18 @@ def delete_record(self, record_id: int | str) -> int | str: RecordNotFoundException: If the record is not found NocoDBException: For other API errors """ - return self.client.delete_record(self.table_id, record_id) + return self.client.delete_record(self.table_id, record_id, base_id=base_id) - def count_records(self, where: str | None = None) -> int: + def count_records( + self, + where: str | None = None, + base_id: str | None = None, + ) -> int: """Count records in the table. Args: where: Filter condition (e.g., "(Name,eq,John)") + base_id: Base ID (required for v3, optional for v2) Returns: Number of records matching the criteria @@ -159,13 +182,18 @@ def count_records(self, where: str | None = None) -> int: Raises: NocoDBException: For API errors """ - return self.client.count_records(self.table_id, where) + return self.client.count_records(self.table_id, where, base_id=base_id) - def bulk_insert_records(self, records: list[dict[str, Any]]) -> list[int | str]: + def bulk_insert_records( + self, + records: list[dict[str, Any]], + base_id: str | None = None, + ) -> list[int | str]: """Insert multiple records at once for better performance. Args: records: List of record dictionaries to insert + base_id: Base ID (required for v3, optional for v2) Returns: List of inserted record IDs @@ -174,13 +202,18 @@ def bulk_insert_records(self, records: list[dict[str, Any]]) -> list[int | str]: NocoDBException: For API errors ValidationException: If records data is invalid """ - return self.client.bulk_insert_records(self.table_id, records) + return self.client.bulk_insert_records(self.table_id, records, base_id=base_id) - def bulk_update_records(self, records: list[dict[str, Any]]) -> list[int | str]: + def bulk_update_records( + self, + records: list[dict[str, Any]], + base_id: str | None = None, + ) -> list[int | str]: """Update multiple records at once for better performance. Args: records: List of record dictionaries to update (must include Id field) + base_id: Base ID (required for v3, optional for v2) Returns: List of updated record IDs @@ -189,13 +222,18 @@ def bulk_update_records(self, records: list[dict[str, Any]]) -> list[int | str]: NocoDBException: For API errors ValidationException: If records data is invalid """ - return self.client.bulk_update_records(self.table_id, records) + return self.client.bulk_update_records(self.table_id, records, base_id=base_id) - def bulk_delete_records(self, record_ids: list[int | str]) -> list[int | str]: + def bulk_delete_records( + self, + record_ids: list[int | str], + base_id: str | None = None, + ) -> list[int | str]: """Delete multiple records at once for better performance. Args: record_ids: List of record IDs to delete + base_id: Base ID (required for v3, optional for v2) Returns: List of deleted record IDs @@ -204,7 +242,7 @@ def bulk_delete_records(self, record_ids: list[int | str]) -> list[int | str]: NocoDBException: For API errors ValidationException: If record_ids is invalid """ - return self.client.bulk_delete_records(self.table_id, record_ids) + return self.client.bulk_delete_records(self.table_id, record_ids, base_id=base_id) def query(self) -> QueryBuilder: """Create a new QueryBuilder for this table. diff --git a/tests/test_base_resolver.py b/tests/test_base_resolver.py new file mode 100644 index 0000000..1cd1d2f --- /dev/null +++ b/tests/test_base_resolver.py @@ -0,0 +1,227 @@ +"""Tests for BaseIdResolver. + +MIT License + +Copyright (c) BAUER GROUP +""" + +from unittest.mock import MagicMock, patch + +import pytest + +from nocodb_simple_client.base_resolver import BaseIdResolver +from nocodb_simple_client.exceptions import TableNotFoundException + + +class TestBaseIdResolver: + """Test BaseIdResolver for base ID resolution and caching.""" + + @pytest.fixture + def mock_client(self): + """Create a mock NocoDB client.""" + client = MagicMock() + return client + + @pytest.fixture + def resolver(self, mock_client): + """Create a BaseIdResolver instance.""" + return BaseIdResolver(mock_client) + + def test_initialization(self, mock_client): + """Test BaseIdResolver initialization.""" + resolver = BaseIdResolver(mock_client) + + assert resolver._client == mock_client + assert resolver._cache == {} + assert resolver._enabled is True + + def test_get_base_id_with_base_id_in_response(self, resolver, mock_client): + """Test getting base_id when it's in the response.""" + mock_client._get.return_value = { + "id": "table_123", + "title": "Test Table", + "base_id": "base_abc", + } + + base_id = resolver.get_base_id("table_123") + + assert base_id == "base_abc" + assert resolver._cache["table_123"] == "base_abc" + mock_client._get.assert_called_once_with("api/v2/meta/tables/table_123") + + def test_get_base_id_with_source_id(self, resolver, mock_client): + """Test getting base_id when response uses source_id.""" + mock_client._get.return_value = { + "id": "table_123", + "title": "Test Table", + "source_id": "base_xyz", + } + + base_id = resolver.get_base_id("table_123") + + assert base_id == "base_xyz" + assert resolver._cache["table_123"] == "base_xyz" + + def test_get_base_id_with_project_id(self, resolver, mock_client): + """Test getting base_id when response uses project_id (legacy).""" + mock_client._get.return_value = { + "id": "table_123", + "title": "Test Table", + "project_id": "base_legacy", + } + + base_id = resolver.get_base_id("table_123") + + assert base_id == "base_legacy" + assert resolver._cache["table_123"] == "base_legacy" + + def test_get_base_id_from_cache(self, resolver, mock_client): + """Test getting base_id from cache (no API call).""" + # Pre-populate cache + resolver._cache["table_123"] = "base_cached" + + base_id = resolver.get_base_id("table_123") + + assert base_id == "base_cached" + # Should not make any API calls + mock_client._get.assert_not_called() + + def test_get_base_id_force_refresh(self, resolver, mock_client): + """Test force refresh bypasses cache.""" + # Pre-populate cache + resolver._cache["table_123"] = "base_old" + + mock_client._get.return_value = { + "id": "table_123", + "base_id": "base_new", + } + + base_id = resolver.get_base_id("table_123", force_refresh=True) + + assert base_id == "base_new" + assert resolver._cache["table_123"] == "base_new" + mock_client._get.assert_called_once() + + def test_get_base_id_not_found(self, resolver, mock_client): + """Test handling when base_id cannot be found.""" + mock_client._get.return_value = { + "id": "table_123", + "title": "Test Table", + # No base_id, source_id, or project_id + } + + with pytest.raises(TableNotFoundException, match="Could not resolve base_id"): + resolver.get_base_id("table_123") + + def test_set_base_id_manually(self, resolver): + """Test manually setting base_id mapping.""" + resolver.set_base_id("table_123", "base_manual") + + assert resolver._cache["table_123"] == "base_manual" + + # Verify it's used when getting + base_id = resolver.get_base_id("table_123") + assert base_id == "base_manual" + + def test_clear_cache_specific_table(self, resolver): + """Test clearing cache for specific table.""" + resolver._cache = { + "table_1": "base_1", + "table_2": "base_2", + "table_3": "base_3", + } + + resolver.clear_cache("table_2") + + assert "table_1" in resolver._cache + assert "table_2" not in resolver._cache + assert "table_3" in resolver._cache + + def test_clear_cache_all(self, resolver): + """Test clearing entire cache.""" + resolver._cache = { + "table_1": "base_1", + "table_2": "base_2", + "table_3": "base_3", + } + + resolver.clear_cache() + + assert resolver._cache == {} + + def test_get_cache_size(self, resolver): + """Test getting cache size.""" + assert resolver.get_cache_size() == 0 + + resolver._cache = { + "table_1": "base_1", + "table_2": "base_2", + } + + assert resolver.get_cache_size() == 2 + + def test_disable_resolver(self, resolver): + """Test disabling the resolver.""" + assert resolver.is_enabled() is True + + resolver.disable() + + assert resolver.is_enabled() is False + + def test_enable_resolver(self, resolver): + """Test enabling the resolver.""" + resolver.disable() + assert resolver.is_enabled() is False + + resolver.enable() + + assert resolver.is_enabled() is True + + def test_multiple_tables_caching(self, resolver, mock_client): + """Test caching works for multiple tables.""" + mock_client._get.side_effect = [ + {"id": "table_1", "base_id": "base_a"}, + {"id": "table_2", "base_id": "base_b"}, + {"id": "table_3", "base_id": "base_c"}, + ] + + # First calls - should hit API + base1 = resolver.get_base_id("table_1") + base2 = resolver.get_base_id("table_2") + base3 = resolver.get_base_id("table_3") + + assert base1 == "base_a" + assert base2 == "base_b" + assert base3 == "base_c" + assert mock_client._get.call_count == 3 + + # Second calls - should use cache + mock_client._get.reset_mock() + + base1_cached = resolver.get_base_id("table_1") + base2_cached = resolver.get_base_id("table_2") + + assert base1_cached == "base_a" + assert base2_cached == "base_b" + assert mock_client._get.call_count == 0 # No API calls + + def test_cache_persistence_across_operations(self, resolver, mock_client): + """Test cache persists across different operations.""" + mock_client._get.return_value = {"id": "table_123", "base_id": "base_abc"} + + # First call + base_id_1 = resolver.get_base_id("table_123") + + # Manual set for different table + resolver.set_base_id("table_456", "base_xyz") + + # Get both + base_id_1_again = resolver.get_base_id("table_123") + base_id_2 = resolver.get_base_id("table_456") + + assert base_id_1 == "base_abc" + assert base_id_1_again == "base_abc" + assert base_id_2 == "base_xyz" + + # Should only have made one API call + assert mock_client._get.call_count == 1 diff --git a/tests/test_table.py b/tests/test_table.py index 875e100..3e614df 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -37,7 +37,7 @@ def test_get_records(self, table, mock_client): assert result == expected_records mock_client.get_records.assert_called_once_with( - "test_table_123", None, "(Status,eq,active)", None, 10 + "test_table_123", base_id=None, sort=None, where="(Status,eq,active)", fields=None, limit=10 ) def test_get_record(self, table, mock_client): @@ -48,7 +48,7 @@ def test_get_record(self, table, mock_client): result = table.get_record("record_123") assert result == expected_record - mock_client.get_record.assert_called_once_with("test_table_123", "record_123", None) + mock_client.get_record.assert_called_once_with("test_table_123", "record_123", None, base_id=None) def test_insert_record(self, table, mock_client): """Test insert_record delegation to client.""" @@ -58,7 +58,7 @@ def test_insert_record(self, table, mock_client): result = table.insert_record(record_data) assert result == "new_record_123" - mock_client.insert_record.assert_called_once_with("test_table_123", record_data) + mock_client.insert_record.assert_called_once_with("test_table_123", record_data, base_id=None) def test_update_record(self, table, mock_client): """Test update_record delegation to client.""" @@ -69,7 +69,7 @@ def test_update_record(self, table, mock_client): assert result == "record_123" mock_client.update_record.assert_called_once_with( - "test_table_123", update_data, "record_123" + "test_table_123", update_data, "record_123", base_id=None ) def test_delete_record(self, table, mock_client): @@ -79,7 +79,7 @@ def test_delete_record(self, table, mock_client): result = table.delete_record("record_123") assert result == "record_123" - mock_client.delete_record.assert_called_once_with("test_table_123", "record_123") + mock_client.delete_record.assert_called_once_with("test_table_123", "record_123", base_id=None) def test_count_records(self, table, mock_client): """Test count_records delegation to client.""" @@ -89,7 +89,7 @@ def test_count_records(self, table, mock_client): assert result == 42 mock_client.count_records.assert_called_once_with( - "test_table_123", "(Status,eq,active)" + "test_table_123", "(Status,eq,active)", base_id=None ) def test_bulk_insert_records(self, table, mock_client): @@ -100,7 +100,7 @@ def test_bulk_insert_records(self, table, mock_client): result = table.bulk_insert_records(records) assert result == ["rec1", "rec2"] - mock_client.bulk_insert_records.assert_called_once_with("test_table_123", records) + mock_client.bulk_insert_records.assert_called_once_with("test_table_123", records, base_id=None) def test_bulk_update_records(self, table, mock_client): """Test bulk_update_records delegation to client.""" @@ -110,7 +110,7 @@ def test_bulk_update_records(self, table, mock_client): result = table.bulk_update_records(records) assert result == ["rec1"] - mock_client.bulk_update_records.assert_called_once_with("test_table_123", records) + mock_client.bulk_update_records.assert_called_once_with("test_table_123", records, base_id=None) def test_bulk_delete_records(self, table, mock_client): """Test bulk_delete_records delegation to client.""" @@ -120,7 +120,7 @@ def test_bulk_delete_records(self, table, mock_client): result = table.bulk_delete_records(record_ids) assert result == ["rec1", "rec2", "rec3"] - mock_client.bulk_delete_records.assert_called_once_with("test_table_123", record_ids) + mock_client.bulk_delete_records.assert_called_once_with("test_table_123", record_ids, base_id=None) def test_attach_file_to_record(self, table, mock_client): """Test file attachment delegation to client.""" diff --git a/tests/test_version_switching.py b/tests/test_version_switching.py new file mode 100644 index 0000000..ec0bb1e --- /dev/null +++ b/tests/test_version_switching.py @@ -0,0 +1,368 @@ +"""Integration tests for API version switching between v2 and v3. + +MIT License + +Copyright (c) BAUER GROUP +""" + +from unittest.mock import MagicMock, patch + +import pytest + +from nocodb_simple_client import NocoDBClient, NocoDBMetaClient +from nocodb_simple_client.api_version import APIVersion + + +class TestClientVersionSwitching: + """Test version switching for NocoDBClient.""" + + @pytest.fixture + def mock_session(self): + """Create a mock session.""" + with patch("nocodb_simple_client.client.requests.Session") as mock: + yield mock.return_value + + def test_client_default_v2(self, mock_session): + """Test client defaults to v2.""" + client = NocoDBClient(base_url="https://test.com", db_auth_token="token") + + assert client.api_version == APIVersion.V2 + assert client.base_id is None + assert client._path_builder is not None + assert client._param_adapter is not None + assert client._base_resolver is None # Only created for v3 + + def test_client_explicit_v2(self, mock_session): + """Test client with explicit v2.""" + client = NocoDBClient( + base_url="https://test.com", db_auth_token="token", api_version="v2" + ) + + assert client.api_version == APIVersion.V2 + + def test_client_explicit_v3(self, mock_session): + """Test client with explicit v3.""" + client = NocoDBClient( + base_url="https://test.com", + db_auth_token="token", + api_version="v3", + base_id="base_123", + ) + + assert client.api_version == APIVersion.V3 + assert client.base_id == "base_123" + assert client._base_resolver is not None # Created for v3 + + def test_client_v3_without_base_id(self, mock_session): + """Test v3 client can be created without base_id.""" + client = NocoDBClient( + base_url="https://test.com", db_auth_token="token", api_version="v3" + ) + + assert client.api_version == APIVersion.V3 + assert client.base_id is None + assert client._base_resolver is not None + + def test_get_records_v2_endpoint(self, mock_session): + """Test get_records uses v2 endpoint.""" + client = NocoDBClient( + base_url="https://test.com", db_auth_token="token", api_version="v2" + ) + + mock_session.get.return_value.json.return_value = {"list": [], "pageInfo": {}} + mock_session.get.return_value.status_code = 200 + + client.get_records("table_123", limit=10) + + # Check that v2 endpoint was called + call_args = mock_session.get.call_args + assert "api/v2/tables/table_123/records" in call_args[0][0] + + def test_get_records_v3_endpoint(self, mock_session): + """Test get_records uses v3 endpoint.""" + client = NocoDBClient( + base_url="https://test.com", + db_auth_token="token", + api_version="v3", + base_id="base_abc", + ) + + mock_session.get.return_value.json.return_value = {"list": [], "pageInfo": {}} + mock_session.get.return_value.status_code = 200 + + client.get_records("table_123", limit=10) + + # Check that v3 endpoint was called + call_args = mock_session.get.call_args + assert "api/v3/data/base_abc/table_123/records" in call_args[0][0] + + def test_v2_pagination_params(self, mock_session): + """Test v2 uses offset/limit parameters.""" + client = NocoDBClient( + base_url="https://test.com", db_auth_token="token", api_version="v2" + ) + + mock_session.get.return_value.json.return_value = {"list": [], "pageInfo": {}} + mock_session.get.return_value.status_code = 200 + + client.get_records("table_123", limit=25) + + # Check parameters + call_args = mock_session.get.call_args + params = call_args[1]["params"] + + assert "limit" in params + assert params["limit"] == 25 + assert "page" not in params + assert "pageSize" not in params + + def test_v3_pagination_params(self, mock_session): + """Test v3 converts to page/pageSize parameters.""" + client = NocoDBClient( + base_url="https://test.com", + db_auth_token="token", + api_version="v3", + base_id="base_abc", + ) + + mock_session.get.return_value.json.return_value = {"list": [], "pageInfo": {}} + mock_session.get.return_value.status_code = 200 + + client.get_records("table_123", limit=25) + + # Check parameters + call_args = mock_session.get.call_args + params = call_args[1]["params"] + + assert "page" in params + assert "pageSize" in params + assert params["page"] == 1 + assert params["pageSize"] == 25 + assert "offset" not in params + assert "limit" not in params + + def test_v2_sort_string_format(self, mock_session): + """Test v2 uses string sort format.""" + client = NocoDBClient( + base_url="https://test.com", db_auth_token="token", api_version="v2" + ) + + mock_session.get.return_value.json.return_value = {"list": [], "pageInfo": {}} + mock_session.get.return_value.status_code = 200 + + client.get_records("table_123", sort="name,-age") + + # Check parameters + call_args = mock_session.get.call_args + params = call_args[1]["params"] + + assert params["sort"] == "name,-age" + + def test_v3_sort_json_format(self, mock_session): + """Test v3 converts sort to JSON format.""" + client = NocoDBClient( + base_url="https://test.com", + db_auth_token="token", + api_version="v3", + base_id="base_abc", + ) + + mock_session.get.return_value.json.return_value = {"list": [], "pageInfo": {}} + mock_session.get.return_value.status_code = 200 + + client.get_records("table_123", sort="name,-age") + + # Check parameters + call_args = mock_session.get.call_args + params = call_args[1]["params"] + + assert isinstance(params["sort"], list) + assert len(params["sort"]) == 2 + assert params["sort"][0] == {"field": "name", "direction": "asc"} + assert params["sort"][1] == {"field": "age", "direction": "desc"} + + +class TestMetaClientVersionSwitching: + """Test version switching for NocoDBMetaClient.""" + + @pytest.fixture + def mock_session(self): + """Create a mock session.""" + with patch("nocodb_simple_client.client.requests.Session") as mock: + yield mock.return_value + + def test_meta_client_default_v2(self, mock_session): + """Test meta client defaults to v2.""" + client = NocoDBMetaClient(base_url="https://test.com", db_auth_token="token") + + assert client.api_version == APIVersion.V2 + + def test_meta_client_explicit_v3(self, mock_session): + """Test meta client with explicit v3.""" + client = NocoDBMetaClient( + base_url="https://test.com", + db_auth_token="token", + api_version="v3", + base_id="base_123", + ) + + assert client.api_version == APIVersion.V3 + assert client.base_id == "base_123" + + def test_list_tables_v2_endpoint(self, mock_session): + """Test list_tables uses v2 endpoint.""" + client = NocoDBMetaClient( + base_url="https://test.com", db_auth_token="token", api_version="v2" + ) + + mock_session.get.return_value.json.return_value = {"list": []} + mock_session.get.return_value.status_code = 200 + + client.list_tables("base_123") + + # Check that v2 endpoint was called + call_args = mock_session.get.call_args + assert "api/v2/meta/bases/base_123/tables" in call_args[0][0] + + def test_list_tables_v3_endpoint(self, mock_session): + """Test list_tables uses v3 endpoint.""" + client = NocoDBMetaClient( + base_url="https://test.com", + db_auth_token="token", + api_version="v3", + base_id="base_abc", + ) + + mock_session.get.return_value.json.return_value = {"list": []} + mock_session.get.return_value.status_code = 200 + + client.list_tables("base_abc") + + # Check that v3 endpoint was called + call_args = mock_session.get.call_args + assert "api/v3/meta/bases/base_abc/tables" in call_args[0][0] + + def test_get_table_info_v2_no_base_id(self, mock_session): + """Test get_table_info in v2 doesn't require base_id.""" + client = NocoDBMetaClient( + base_url="https://test.com", db_auth_token="token", api_version="v2" + ) + + mock_session.get.return_value.json.return_value = {"id": "table_123"} + mock_session.get.return_value.status_code = 200 + + client.get_table_info("table_123") + + # Check endpoint + call_args = mock_session.get.call_args + assert "api/v2/meta/tables/table_123" in call_args[0][0] + + def test_get_table_info_v3_with_base_id(self, mock_session): + """Test get_table_info in v3 uses base_id.""" + client = NocoDBMetaClient( + base_url="https://test.com", + db_auth_token="token", + api_version="v3", + base_id="base_abc", + ) + + mock_session.get.return_value.json.return_value = {"id": "table_123"} + mock_session.get.return_value.status_code = 200 + + client.get_table_info("table_123") + + # Check endpoint includes base_id + call_args = mock_session.get.call_args + assert "api/v3/meta/bases/base_abc/tables/table_123" in call_args[0][0] + + def test_columns_v2_terminology(self, mock_session): + """Test v2 uses 'columns' terminology.""" + client = NocoDBMetaClient( + base_url="https://test.com", db_auth_token="token", api_version="v2" + ) + + mock_session.get.return_value.json.return_value = {"list": []} + mock_session.get.return_value.status_code = 200 + + client.list_columns("table_123") + + # Check endpoint uses "columns" + call_args = mock_session.get.call_args + assert "columns" in call_args[0][0] + assert "fields" not in call_args[0][0] + + def test_columns_v3_becomes_fields(self, mock_session): + """Test v3 uses 'fields' terminology.""" + client = NocoDBMetaClient( + base_url="https://test.com", + db_auth_token="token", + api_version="v3", + base_id="base_abc", + ) + + mock_session.get.return_value.json.return_value = {"list": []} + mock_session.get.return_value.status_code = 200 + + # API is still list_columns, but endpoint uses "fields" + client.list_columns("table_123") + + # Check endpoint uses "fields" + call_args = mock_session.get.call_args + assert "fields" in call_args[0][0] + assert "columns" not in call_args[0][0] + + +class TestCrossFunctionalityBetweenVersions: + """Test that clients work correctly across different features.""" + + @pytest.fixture + def mock_session(self): + """Create a mock session.""" + with patch("nocodb_simple_client.client.requests.Session") as mock: + yield mock.return_value + + def test_file_upload_v2_v3_paths(self, mock_session): + """Test file upload uses correct paths for v2 and v3.""" + # v2 client + client_v2 = NocoDBClient( + base_url="https://test.com", db_auth_token="token", api_version="v2" + ) + + # v3 client + client_v3 = NocoDBClient( + base_url="https://test.com", + db_auth_token="token", + api_version="v3", + base_id="base_abc", + ) + + # Check path construction + v2_path = client_v2._path_builder.file_upload("table_123") + v3_path = client_v3._path_builder.file_upload("table_123", "base_abc") + + assert "api/v2/tables/table_123/attachments" in v2_path + assert "api/v3/data/base_abc/table_123/attachments" in v3_path + + def test_both_data_and_meta_operations(self, mock_session): + """Test client can perform both data and meta operations.""" + # Create v3 meta client + meta_client = NocoDBMetaClient( + base_url="https://test.com", + db_auth_token="token", + api_version="v3", + base_id="base_abc", + ) + + mock_session.get.return_value.json.return_value = {"list": []} + mock_session.get.return_value.status_code = 200 + + # Meta operation + meta_client.list_tables("base_abc") + meta_call = mock_session.get.call_args[0][0] + assert "api/v3/meta/bases/base_abc/tables" in meta_call + + # Data operation (inherited from NocoDBClient) + mock_session.get.return_value.json.return_value = {"list": [], "pageInfo": {}} + meta_client.get_records("table_123") + data_call = mock_session.get.call_args[0][0] + assert "api/v3/data/base_abc/table_123/records" in data_call From 9e1317d203a264b2632766d0a8ffd5d4f38c2a22 Mon Sep 17 00:00:00 2001 From: Karl Bauer Date: Fri, 10 Oct 2025 14:48:06 +0200 Subject: [PATCH 6/7] feat: Refactor parameter handling and type annotations in API client and resolver classes --- src/nocodb_simple_client/api_version.py | 16 ++++++++-------- src/nocodb_simple_client/base_resolver.py | 7 ++++--- src/nocodb_simple_client/client.py | 7 ++++++- src/nocodb_simple_client/table.py | 2 +- tests/test_base_resolver.py | 2 +- tests/test_table.py | 2 +- tests/test_version_switching.py | 2 +- 7 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/nocodb_simple_client/api_version.py b/src/nocodb_simple_client/api_version.py index 6d26d48..f8bf3cb 100644 --- a/src/nocodb_simple_client/api_version.py +++ b/src/nocodb_simple_client/api_version.py @@ -172,9 +172,9 @@ def convert_where_operators_to_v3(where: dict[str, Any] | None) -> dict[str, Any # Deep copy to avoid modifying original import json - result = json.loads(json.dumps(where)) + result: dict[str, Any] = json.loads(json.dumps(where)) - def replace_ne(obj: Any) -> Any: + def replace_ne(obj: Any) -> None: if isinstance(obj, dict): # Replace 'ne' with 'neq' if "ne" in obj: @@ -185,9 +185,9 @@ def replace_ne(obj: Any) -> Any: elif isinstance(obj, list): for item in obj: replace_ne(item) - return obj - return replace_ne(result) + replace_ne(result) + return result @staticmethod def convert_where_operators_to_v2(where: dict[str, Any] | None) -> dict[str, Any] | None: @@ -207,9 +207,9 @@ def convert_where_operators_to_v2(where: dict[str, Any] | None) -> dict[str, Any # Deep copy to avoid modifying original import json - result = json.loads(json.dumps(where)) + result: dict[str, Any] = json.loads(json.dumps(where)) - def replace_neq(obj: Any) -> Any: + def replace_neq(obj: Any) -> None: if isinstance(obj, dict): # Replace 'neq' with 'ne' if "neq" in obj: @@ -220,9 +220,9 @@ def replace_neq(obj: Any) -> Any: elif isinstance(obj, list): for item in obj: replace_neq(item) - return obj - return replace_neq(result) + replace_neq(result) + return result class PathBuilder: diff --git a/src/nocodb_simple_client/base_resolver.py b/src/nocodb_simple_client/base_resolver.py index 34dc427..4e8cd07 100644 --- a/src/nocodb_simple_client/base_resolver.py +++ b/src/nocodb_simple_client/base_resolver.py @@ -75,20 +75,21 @@ def get_base_id(self, table_id: str, force_refresh: bool = False) -> str: # This uses the v2 endpoint which doesn't require base_id table_info = self._client._get(f"api/v2/meta/tables/{table_id}") + base_id: str if not table_info or "base_id" not in table_info: # Try alternative response structure if "source_id" in table_info: # In some NocoDB versions, it's called source_id - base_id = table_info["source_id"] + base_id = str(table_info["source_id"]) elif "project_id" in table_info: # Or project_id in older versions - base_id = table_info["project_id"] + base_id = str(table_info["project_id"]) else: # If we can't find it, try to extract from fk_model_id or similar # As a fallback, we might need to list all bases and find the table base_id = self._find_base_id_from_list(table_id) else: - base_id = table_info["base_id"] + base_id = str(table_info["base_id"]) # Cache the result self._cache[table_id] = base_id diff --git a/src/nocodb_simple_client/client.py b/src/nocodb_simple_client/client.py index 12abe84..11c412d 100644 --- a/src/nocodb_simple_client/client.py +++ b/src/nocodb_simple_client/client.py @@ -332,7 +332,12 @@ def get_records( while remaining_limit > 0: batch_limit = min(remaining_limit, 100) # NocoDB max limit per request - params = {"sort": sort, "where": where, "limit": batch_limit, "offset": offset} + params: dict[str, Any] = { + "sort": sort, + "where": where, + "limit": batch_limit, + "offset": offset, + } if fields: params["fields"] = ",".join(fields) diff --git a/src/nocodb_simple_client/table.py b/src/nocodb_simple_client/table.py index 73cc7a6..a2bb51c 100644 --- a/src/nocodb_simple_client/table.py +++ b/src/nocodb_simple_client/table.py @@ -102,7 +102,7 @@ def get_record( RecordNotFoundException: If the record is not found NocoDBException: For other API errors """ - return self.client.get_record(self.table_id, record_id, fields, base_id=base_id) + return self.client.get_record(self.table_id, record_id, base_id=base_id, fields=fields) def insert_record( self, diff --git a/tests/test_base_resolver.py b/tests/test_base_resolver.py index 1cd1d2f..10f98d3 100644 --- a/tests/test_base_resolver.py +++ b/tests/test_base_resolver.py @@ -5,7 +5,7 @@ Copyright (c) BAUER GROUP """ -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock import pytest diff --git a/tests/test_table.py b/tests/test_table.py index 3e614df..6d22e9e 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -48,7 +48,7 @@ def test_get_record(self, table, mock_client): result = table.get_record("record_123") assert result == expected_record - mock_client.get_record.assert_called_once_with("test_table_123", "record_123", None, base_id=None) + mock_client.get_record.assert_called_once_with("test_table_123", "record_123", base_id=None, fields=None) def test_insert_record(self, table, mock_client): """Test insert_record delegation to client.""" diff --git a/tests/test_version_switching.py b/tests/test_version_switching.py index ec0bb1e..1faef65 100644 --- a/tests/test_version_switching.py +++ b/tests/test_version_switching.py @@ -5,7 +5,7 @@ Copyright (c) BAUER GROUP """ -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest From f0dea7cf9bb8cf2d862bba1c50a9857be9f97d25 Mon Sep 17 00:00:00 2001 From: Karl Bauer Date: Fri, 10 Oct 2025 14:59:12 +0200 Subject: [PATCH 7/7] feat: Update file upload paths for API v2 to use new endpoint structure --- src/nocodb_simple_client/api_version.py | 2 +- tests/test_api_version.py | 2 +- tests/test_version_switching.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/nocodb_simple_client/api_version.py b/src/nocodb_simple_client/api_version.py index f8bf3cb..0cecf9b 100644 --- a/src/nocodb_simple_client/api_version.py +++ b/src/nocodb_simple_client/api_version.py @@ -466,7 +466,7 @@ def file_upload(self, table_id: str, base_id: str | None = None) -> str: API endpoint path """ if self.api_version == APIVersion.V2: - return f"api/v2/tables/{table_id}/attachments" + return "api/v2/storage/upload" else: # V3 if not base_id: raise ValueError("base_id is required for API v3") diff --git a/tests/test_api_version.py b/tests/test_api_version.py index 71bd1e2..9b5aebc 100644 --- a/tests/test_api_version.py +++ b/tests/test_api_version.py @@ -253,7 +253,7 @@ def test_file_upload_v2(self): builder = PathBuilder(APIVersion.V2) path = builder.file_upload("table_123") - assert path == "api/v2/tables/table_123/attachments" + assert path == "api/v2/storage/upload" def test_file_upload_v3(self): """Test file upload path for v3.""" diff --git a/tests/test_version_switching.py b/tests/test_version_switching.py index 1faef65..84f7293 100644 --- a/tests/test_version_switching.py +++ b/tests/test_version_switching.py @@ -340,8 +340,8 @@ def test_file_upload_v2_v3_paths(self, mock_session): v2_path = client_v2._path_builder.file_upload("table_123") v3_path = client_v3._path_builder.file_upload("table_123", "base_abc") - assert "api/v2/tables/table_123/attachments" in v2_path - assert "api/v3/data/base_abc/table_123/attachments" in v3_path + assert v2_path == "api/v2/storage/upload" + assert v3_path == "api/v3/data/base_abc/table_123/attachments" def test_both_data_and_meta_operations(self, mock_session): """Test client can perform both data and meta operations."""