update: workspace lazy load

This commit is contained in:
tao.chen
2026-08-12 18:36:17 +08:00
parent c6c2481b96
commit 495018de34
3 changed files with 152 additions and 2 deletions
+40 -2
View File
@@ -73,9 +73,12 @@
> 是它在对象存储里的"工作副本",`Versions` 是 immutable 的稳定版本。
> 写操作受 **is_locked 门禁 + owner 校验** 保护(架构 V3.1 §4)。
### 3.1 `GET /api/v1/workspace-tree`
### 3.1 `GET /api/v1/workspace-tree` (**legacy / 全量视图**)
列出当前用户在 workspace 内的**目录树**。
> **Deprecated**: 新代码请走 §3.3.1 (按 `parent_path` 单层)。该接口仍保留供调试 / 兼容使用,
> 谓词已统一为 `object_status='available' AND is_deleted=0`,与 §3.3.1 保持一致。
列出当前用户在 workspace 内的**整个目录树**(扁平数组)。
- **来源**: 显式 `StorageObjects` 行 (`object_type='directory'`,见 §3.2) **并入**`Scripts.relative_path` 派生的祖先目录,**去重**。空目录(只有显式行、没有文件)也会出现。
- **鉴权**: workspace 成员
@@ -145,6 +148,41 @@
}
```
### 3.3.1 `GET /api/v1/workspace-directories?parent_path=...`
列出指定父目录下的**直接子目录**(单层),用于前端懒加载。**新代码请走本接口**;§3.1 仅作为 legacy / 全量保留。
- **行为**: `parent_path` 为空字符串或缺省 → 用户根目录;非空 → 该父目录的直接子目录(workspace-relative)。仅返回 `object_status='available' AND is_deleted=0` 的 `StorageObjects` 行。
- **鉴权**: workspace 成员
- **查询参数**:
| 名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `parent_path` | string | 否 | 父目录相对路径,空字符串或缺省表示用户根目录 |
- **谓词(SQL 等价)**: `relative_path LIKE '<prefix>/%' AND relative_path NOT LIKE '<prefix>/%/%'`,其中 `prefix = scoped_prefix/{parent_path}`,索引走 `idx_storage_workspace_relative_path(workspace_id, relative_path(255))`。
- **响应**:
```json
{
"request_id": "...",
"data": {
"directories": [
{"path": "scripts", "name": "scripts", "parent_path": "", "has_children": true},
{"path": "scripts/etl", "name": "etl", "parent_path": "scripts", "has_children": false},
{"path": "scripts/etl/daily", "name": "daily", "parent_path": "scripts/etl", "has_children": false}
]
},
"meta": {"directory_count": 3}
}
```
- 字段表(继承 §3.1):
| 字段 | 类型 | 说明 |
|---|---|---|
| `path` | string | workspace 内相对路径 |
| `name` | string | `path` 的最后一段 |
| `parent_path` | string | 父目录相对路径,根目录用空串 |
| `has_children` | bool | 该目录下是否还有直接子目录(后端额外 `EXISTS` 查询,可为空目录为 `false`) |
- **空结果**: 不返回 404,空目录列表即 `directories: []`。
- **错误**: 401(未登录)/ 403(非 workspace 成员)同其他接口。
### 3.4 `GET /api/v1/scripts`
列出当前 workspace 内**全部 active 脚本**。不受 is_locked 影响(读路径不锁)。
+74
View File
@@ -623,6 +623,7 @@ async def get_workspace_tree(
select(StorageObjects.relative_path, StorageObjects.object_type).where(
StorageObjects.workspace_id == context.workspace.workspace_id,
StorageObjects.object_status == "available",
StorageObjects.is_deleted == 0,
StorageObjects.relative_path.like(like_prefix),
)
)
@@ -664,6 +665,74 @@ async def get_workspace_tree(
},
)
sorted_dirs = sorted(directories.values(), key=lambda item: item["path"])
return {
"request_id": context.request_id,
"data": {"directories": sorted_dirs},
"meta": {"directory_count": len(sorted_dirs)},
}
@router.get("/api/v1/workspace-directories")
async def list_workspace_directories(
parent_path: str = Query(default=""),
context: RequestContext = Depends(request_context),
session: AsyncSession = Depends(database_session),
) -> dict[str, Any]:
"""List direct child directories of a workspace path.
Empty ``parent_path`` returns the directories immediately under the
user's scoped root. Only available, non-deleted StorageObjects are
considered.
"""
scoped_prefix = user_relative_path(context)
parent = normalize_user_path(parent_path)
target_prefix = f"{scoped_prefix}/{parent}" if parent else scoped_prefix
descendant_prefix = f"{target_prefix}/"
rows = (
await session.execute(
select(StorageObjects.relative_path, StorageObjects.object_type).where(
StorageObjects.workspace_id == context.workspace.workspace_id,
StorageObjects.object_status == "available",
StorageObjects.is_deleted == 0,
StorageObjects.relative_path.like(f"{descendant_prefix}%"),
~StorageObjects.relative_path.like(f"{descendant_prefix}%/%"),
)
)
).all()
directories: dict[str, dict[str, Any]] = {}
for relative, _object_type in rows:
if not relative or not relative.startswith(descendant_prefix):
continue
suffix = relative[len(descendant_prefix) :]
if "/" in suffix:
continue
child_path = f"{parent}/{suffix}" if parent else suffix
directories.setdefault(
child_path,
{
"path": child_path,
"name": suffix,
"parent_path": parent,
"has_children": False,
},
)
for directory in directories.values():
child_prefix = f"{target_prefix}/{directory['path']}/"
has_children = await session.scalar(
select(StorageObjects.storage_object_id).where(
StorageObjects.workspace_id == context.workspace.workspace_id,
StorageObjects.object_status == "available",
StorageObjects.is_deleted == 0,
StorageObjects.relative_path.like(f"{child_prefix}%"),
~StorageObjects.relative_path.like(f"{child_prefix}%/%"),
).limit(1)
)
directory["has_children"] = has_children is not None
sorted_dirs = sorted(directories.values(), key=lambda item: item["path"])
return {
"request_id": context.request_id,
"data": {"directories": sorted_dirs},
@@ -1160,6 +1229,11 @@ async def delete_script(
detail=exc.detail,
) from exc
if storage_object:
storage_object.object_status = "deleted"
storage_object.is_deleted = 1
storage_object.deleted_at = datetime.now(UTC).replace(tzinfo=None)
script.status = "deleted"
script.deleted_at = datetime.now(UTC).replace(tzinfo=None)
return {
@@ -0,0 +1,38 @@
"""Add workspace tree relative_path index.
The ``list_workspace_directories`` endpoint filters by
``relative_path`` prefixes inside a workspace. A composite index on
``(workspace_id, relative_path(255))`` avoids scanning all rows for a
workspace when listing a subdirectory.
Revision ID: 3ba4d8489f36
Revises: f6a7b8c9d0e1
Create Date: 2026-08-12
"""
from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "3ba4d8489f36"
down_revision: str | Sequence[str] | None = "f6a7b8c9d0e1"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.create_index(
"idx_storage_workspace_relative_path",
"storage_objects",
[sa.text("`workspace_id`"), sa.text("`relative_path`(255)")],
unique=False,
)
def downgrade() -> None:
op.drop_index(
"idx_storage_workspace_relative_path",
table_name="storage_objects",
)