@weirdfingers/baseboards 0.9.6 → 0.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +560 -469
- package/dist/index.js.map +1 -1
- package/package.json +2 -5
- package/templates/README.md +0 -122
- package/templates/api/.env.example +0 -65
- package/templates/api/ARTIFACT_RESOLUTION_GUIDE.md +0 -148
- package/templates/api/Dockerfile +0 -32
- package/templates/api/README.md +0 -264
- package/templates/api/alembic/env.py +0 -114
- package/templates/api/alembic/script.py.mako +0 -28
- package/templates/api/alembic/versions/20250101_000000_initial_schema.py +0 -506
- package/templates/api/alembic/versions/20251022_174729_remove_provider_name_from_generations.py +0 -75
- package/templates/api/alembic/versions/20251023_165852_switch_to_declarative_base_and_mapping.py +0 -467
- package/templates/api/alembic/versions/20251202_000000_add_artifact_lineage.py +0 -134
- package/templates/api/alembic/versions/2025925_62735_add_seed_data_for_default_tenant.py +0 -88
- package/templates/api/alembic.ini +0 -36
- package/templates/api/config/generators.yaml +0 -237
- package/templates/api/config/storage_config.yaml +0 -26
- package/templates/api/docs/ADDING_GENERATORS.md +0 -409
- package/templates/api/docs/GENERATORS_API.md +0 -502
- package/templates/api/docs/MIGRATIONS.md +0 -472
- package/templates/api/docs/TESTING_LIVE_APIS.md +0 -417
- package/templates/api/docs/storage_providers.md +0 -337
- package/templates/api/pyproject.toml +0 -205
- package/templates/api/src/boards/__init__.py +0 -10
- package/templates/api/src/boards/api/app.py +0 -172
- package/templates/api/src/boards/api/auth.py +0 -75
- package/templates/api/src/boards/api/endpoints/__init__.py +0 -3
- package/templates/api/src/boards/api/endpoints/jobs.py +0 -76
- package/templates/api/src/boards/api/endpoints/setup.py +0 -505
- package/templates/api/src/boards/api/endpoints/sse.py +0 -129
- package/templates/api/src/boards/api/endpoints/storage.py +0 -155
- package/templates/api/src/boards/api/endpoints/tenant_registration.py +0 -296
- package/templates/api/src/boards/api/endpoints/uploads.py +0 -149
- package/templates/api/src/boards/api/endpoints/webhooks.py +0 -13
- package/templates/api/src/boards/auth/__init__.py +0 -15
- package/templates/api/src/boards/auth/adapters/__init__.py +0 -27
- package/templates/api/src/boards/auth/adapters/auth0.py +0 -220
- package/templates/api/src/boards/auth/adapters/base.py +0 -73
- package/templates/api/src/boards/auth/adapters/clerk.py +0 -172
- package/templates/api/src/boards/auth/adapters/jwt.py +0 -122
- package/templates/api/src/boards/auth/adapters/none.py +0 -102
- package/templates/api/src/boards/auth/adapters/oidc.py +0 -284
- package/templates/api/src/boards/auth/adapters/supabase.py +0 -110
- package/templates/api/src/boards/auth/context.py +0 -35
- package/templates/api/src/boards/auth/factory.py +0 -129
- package/templates/api/src/boards/auth/middleware.py +0 -221
- package/templates/api/src/boards/auth/provisioning.py +0 -129
- package/templates/api/src/boards/auth/tenant_extraction.py +0 -278
- package/templates/api/src/boards/cli.py +0 -354
- package/templates/api/src/boards/config.py +0 -131
- package/templates/api/src/boards/database/__init__.py +0 -7
- package/templates/api/src/boards/database/cli.py +0 -110
- package/templates/api/src/boards/database/connection.py +0 -292
- package/templates/api/src/boards/database/models.py +0 -19
- package/templates/api/src/boards/database/seed_data.py +0 -182
- package/templates/api/src/boards/dbmodels/__init__.py +0 -441
- package/templates/api/src/boards/generators/__init__.py +0 -57
- package/templates/api/src/boards/generators/artifact_resolution.py +0 -405
- package/templates/api/src/boards/generators/artifacts.py +0 -53
- package/templates/api/src/boards/generators/base.py +0 -144
- package/templates/api/src/boards/generators/implementations/__init__.py +0 -14
- package/templates/api/src/boards/generators/implementations/fal/__init__.py +0 -25
- package/templates/api/src/boards/generators/implementations/fal/audio/__init__.py +0 -23
- package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_music_generation.py +0 -171
- package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_sound_effect_generation.py +0 -167
- package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_text_to_speech.py +0 -176
- package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_tts_turbo.py +0 -195
- package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_sound_effects_v2.py +0 -194
- package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_tts_eleven_v3.py +0 -209
- package/templates/api/src/boards/generators/implementations/fal/audio/fal_elevenlabs_tts_turbo_v2_5.py +0 -206
- package/templates/api/src/boards/generators/implementations/fal/audio/fal_minimax_speech_26_hd.py +0 -237
- package/templates/api/src/boards/generators/implementations/fal/audio/minimax_music_v2.py +0 -173
- package/templates/api/src/boards/generators/implementations/fal/audio/minimax_speech_2_6_turbo.py +0 -221
- package/templates/api/src/boards/generators/implementations/fal/image/__init__.py +0 -63
- package/templates/api/src/boards/generators/implementations/fal/image/bytedance_seedream_v45_edit.py +0 -219
- package/templates/api/src/boards/generators/implementations/fal/image/clarity_upscaler.py +0 -220
- package/templates/api/src/boards/generators/implementations/fal/image/crystal_upscaler.py +0 -173
- package/templates/api/src/boards/generators/implementations/fal/image/fal_ideogram_character.py +0 -227
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2.py +0 -203
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2_edit.py +0 -230
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro.py +0 -204
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro_edit.py +0 -221
- package/templates/api/src/boards/generators/implementations/fal/image/flux_pro_kontext.py +0 -216
- package/templates/api/src/boards/generators/implementations/fal/image/flux_pro_ultra.py +0 -197
- package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image.py +0 -177
- package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image_edit.py +0 -208
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_15_edit.py +0 -216
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_5.py +0 -177
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_edit_image.py +0 -182
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_mini.py +0 -167
- package/templates/api/src/boards/generators/implementations/fal/image/ideogram_character_edit.py +0 -299
- package/templates/api/src/boards/generators/implementations/fal/image/ideogram_v2.py +0 -190
- package/templates/api/src/boards/generators/implementations/fal/image/imagen4_preview.py +0 -191
- package/templates/api/src/boards/generators/implementations/fal/image/imagen4_preview_fast.py +0 -179
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana.py +0 -183
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_edit.py +0 -212
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro.py +0 -179
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro_edit.py +0 -226
- package/templates/api/src/boards/generators/implementations/fal/image/qwen_image.py +0 -249
- package/templates/api/src/boards/generators/implementations/fal/image/qwen_image_edit.py +0 -244
- package/templates/api/src/boards/generators/implementations/fal/image/reve_edit.py +0 -178
- package/templates/api/src/boards/generators/implementations/fal/image/reve_text_to_image.py +0 -155
- package/templates/api/src/boards/generators/implementations/fal/image/seedream_v45_text_to_image.py +0 -180
- package/templates/api/src/boards/generators/implementations/fal/utils.py +0 -61
- package/templates/api/src/boards/generators/implementations/fal/video/__init__.py +0 -77
- package/templates/api/src/boards/generators/implementations/fal/video/bytedance_seedance_v1_pro_text_to_video.py +0 -209
- package/templates/api/src/boards/generators/implementations/fal/video/creatify_lipsync.py +0 -161
- package/templates/api/src/boards/generators/implementations/fal/video/fal_bytedance_seedance_v1_pro_image_to_video.py +0 -222
- package/templates/api/src/boards/generators/implementations/fal/video/fal_minimax_hailuo_02_standard_text_to_video.py +0 -152
- package/templates/api/src/boards/generators/implementations/fal/video/fal_pixverse_lipsync.py +0 -197
- package/templates/api/src/boards/generators/implementations/fal/video/fal_sora_2_text_to_video.py +0 -173
- package/templates/api/src/boards/generators/implementations/fal/video/infinitalk.py +0 -221
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_pro.py +0 -168
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_standard.py +0 -159
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_image_to_video.py +0 -175
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_text_to_video.py +0 -168
- package/templates/api/src/boards/generators/implementations/fal/video/minimax_hailuo_2_3_pro_image_to_video.py +0 -153
- package/templates/api/src/boards/generators/implementations/fal/video/sora2_image_to_video.py +0 -172
- package/templates/api/src/boards/generators/implementations/fal/video/sora_2_image_to_video_pro.py +0 -175
- package/templates/api/src/boards/generators/implementations/fal/video/sora_2_text_to_video_pro.py +0 -163
- package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2.py +0 -167
- package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2_pro.py +0 -155
- package/templates/api/src/boards/generators/implementations/fal/video/veed_fabric_1_0.py +0 -180
- package/templates/api/src/boards/generators/implementations/fal/video/veed_lipsync.py +0 -174
- package/templates/api/src/boards/generators/implementations/fal/video/veo3.py +0 -194
- package/templates/api/src/boards/generators/implementations/fal/video/veo31.py +0 -190
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast.py +0 -190
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast_image_to_video.py +0 -191
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_first_last_frame_to_video.py +0 -187
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_image_to_video.py +0 -183
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_reference_to_video.py +0 -172
- package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_image_to_video.py +0 -212
- package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_text_to_video.py +0 -208
- package/templates/api/src/boards/generators/implementations/fal/video/wan_pro_image_to_video.py +0 -158
- package/templates/api/src/boards/generators/implementations/kie/__init__.py +0 -11
- package/templates/api/src/boards/generators/implementations/kie/base.py +0 -316
- package/templates/api/src/boards/generators/implementations/kie/image/__init__.py +0 -3
- package/templates/api/src/boards/generators/implementations/kie/image/nano_banana_edit.py +0 -190
- package/templates/api/src/boards/generators/implementations/kie/utils.py +0 -98
- package/templates/api/src/boards/generators/implementations/kie/video/__init__.py +0 -8
- package/templates/api/src/boards/generators/implementations/kie/video/veo3.py +0 -161
- package/templates/api/src/boards/generators/implementations/openai/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/openai/audio/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/openai/audio/whisper.py +0 -69
- package/templates/api/src/boards/generators/implementations/openai/image/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/openai/image/dalle3.py +0 -96
- package/templates/api/src/boards/generators/implementations/replicate/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/replicate/image/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/replicate/image/flux_pro.py +0 -88
- package/templates/api/src/boards/generators/implementations/replicate/video/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/replicate/video/lipsync.py +0 -73
- package/templates/api/src/boards/generators/loader.py +0 -253
- package/templates/api/src/boards/generators/registry.py +0 -114
- package/templates/api/src/boards/generators/resolution.py +0 -632
- package/templates/api/src/boards/generators/testmods/class_gen.py +0 -34
- package/templates/api/src/boards/generators/testmods/import_side_effect.py +0 -35
- package/templates/api/src/boards/graphql/__init__.py +0 -7
- package/templates/api/src/boards/graphql/access_control.py +0 -136
- package/templates/api/src/boards/graphql/mutations/root.py +0 -148
- package/templates/api/src/boards/graphql/queries/root.py +0 -116
- package/templates/api/src/boards/graphql/resolvers/__init__.py +0 -8
- package/templates/api/src/boards/graphql/resolvers/auth.py +0 -12
- package/templates/api/src/boards/graphql/resolvers/board.py +0 -1053
- package/templates/api/src/boards/graphql/resolvers/generation.py +0 -666
- package/templates/api/src/boards/graphql/resolvers/generator.py +0 -50
- package/templates/api/src/boards/graphql/resolvers/lineage.py +0 -381
- package/templates/api/src/boards/graphql/resolvers/upload.py +0 -463
- package/templates/api/src/boards/graphql/resolvers/user.py +0 -25
- package/templates/api/src/boards/graphql/schema.py +0 -81
- package/templates/api/src/boards/graphql/types/board.py +0 -102
- package/templates/api/src/boards/graphql/types/generation.py +0 -166
- package/templates/api/src/boards/graphql/types/generator.py +0 -17
- package/templates/api/src/boards/graphql/types/user.py +0 -47
- package/templates/api/src/boards/jobs/repository.py +0 -153
- package/templates/api/src/boards/logging.py +0 -195
- package/templates/api/src/boards/middleware.py +0 -339
- package/templates/api/src/boards/progress/__init__.py +0 -4
- package/templates/api/src/boards/progress/models.py +0 -25
- package/templates/api/src/boards/progress/publisher.py +0 -64
- package/templates/api/src/boards/py.typed +0 -0
- package/templates/api/src/boards/redis_pool.py +0 -118
- package/templates/api/src/boards/storage/__init__.py +0 -52
- package/templates/api/src/boards/storage/base.py +0 -363
- package/templates/api/src/boards/storage/config.py +0 -187
- package/templates/api/src/boards/storage/factory.py +0 -288
- package/templates/api/src/boards/storage/implementations/__init__.py +0 -27
- package/templates/api/src/boards/storage/implementations/gcs.py +0 -340
- package/templates/api/src/boards/storage/implementations/local.py +0 -201
- package/templates/api/src/boards/storage/implementations/s3.py +0 -294
- package/templates/api/src/boards/storage/implementations/supabase.py +0 -218
- package/templates/api/src/boards/tenant_isolation.py +0 -446
- package/templates/api/src/boards/validation.py +0 -262
- package/templates/api/src/boards/workers/__init__.py +0 -1
- package/templates/api/src/boards/workers/actors.py +0 -274
- package/templates/api/src/boards/workers/cli.py +0 -125
- package/templates/api/src/boards/workers/context.py +0 -348
- package/templates/api/src/boards/workers/middleware.py +0 -58
- package/templates/api/src/py.typed +0 -0
- package/templates/compose.web.yaml +0 -35
- package/templates/compose.yaml +0 -116
- package/templates/docker/env.example +0 -23
- package/templates/web/.env.example +0 -28
- package/templates/web/Dockerfile +0 -51
- package/templates/web/components.json +0 -22
- package/templates/web/imageLoader.js +0 -18
- package/templates/web/next-env.d.ts +0 -5
- package/templates/web/next.config.js +0 -36
- package/templates/web/package.json +0 -41
- package/templates/web/postcss.config.mjs +0 -7
- package/templates/web/public/favicon.ico +0 -0
- package/templates/web/src/app/boards/[boardId]/page.tsx +0 -353
- package/templates/web/src/app/globals.css +0 -123
- package/templates/web/src/app/layout.tsx +0 -31
- package/templates/web/src/app/lineage/[generationId]/page.tsx +0 -235
- package/templates/web/src/app/page.tsx +0 -35
- package/templates/web/src/app/providers.tsx +0 -18
- package/templates/web/src/components/boards/ArtifactInputSlots.tsx +0 -206
- package/templates/web/src/components/boards/ArtifactPreview.tsx +0 -466
- package/templates/web/src/components/boards/GenerationGrid.tsx +0 -282
- package/templates/web/src/components/boards/GenerationInput.tsx +0 -370
- package/templates/web/src/components/boards/GeneratorSelector.tsx +0 -272
- package/templates/web/src/components/boards/UploadArtifact.tsx +0 -563
- package/templates/web/src/components/header.tsx +0 -32
- package/templates/web/src/components/theme-provider.tsx +0 -10
- package/templates/web/src/components/theme-toggle.tsx +0 -75
- package/templates/web/src/components/ui/alert-dialog.tsx +0 -157
- package/templates/web/src/components/ui/button.tsx +0 -58
- package/templates/web/src/components/ui/card.tsx +0 -92
- package/templates/web/src/components/ui/dropdown-menu.tsx +0 -200
- package/templates/web/src/components/ui/navigation-menu.tsx +0 -168
- package/templates/web/src/components/ui/toast.tsx +0 -128
- package/templates/web/src/components/ui/toaster.tsx +0 -35
- package/templates/web/src/components/ui/use-toast.ts +0 -187
- package/templates/web/src/hooks/useGeneratorMRU.ts +0 -57
- package/templates/web/src/lib/utils.ts +0 -6
- package/templates/web/tsconfig.json +0 -41
|
@@ -1,1053 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import TYPE_CHECKING
|
|
4
|
-
from uuid import UUID
|
|
5
|
-
|
|
6
|
-
import strawberry
|
|
7
|
-
from sqlalchemy import and_, or_, select
|
|
8
|
-
from sqlalchemy.orm import selectinload
|
|
9
|
-
|
|
10
|
-
from ...database.connection import get_async_session
|
|
11
|
-
from ...dbmodels import BoardMembers, Boards, Generations, Users
|
|
12
|
-
from ...logging import get_logger
|
|
13
|
-
from ..access_control import (
|
|
14
|
-
BoardQueryRole,
|
|
15
|
-
SortOrder,
|
|
16
|
-
can_access_board,
|
|
17
|
-
can_access_board_details,
|
|
18
|
-
ensure_preloaded,
|
|
19
|
-
get_auth_context_from_info,
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
if TYPE_CHECKING:
|
|
23
|
-
from ..mutations.root import AddBoardMemberInput, CreateBoardInput, UpdateBoardInput
|
|
24
|
-
from ..types.board import Board, BoardMember, BoardRole
|
|
25
|
-
from ..types.generation import Generation
|
|
26
|
-
from ..types.user import User
|
|
27
|
-
|
|
28
|
-
logger = get_logger(__name__)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
# Query resolvers
|
|
32
|
-
async def resolve_board_by_id(info: strawberry.Info, id: UUID) -> Board | None:
|
|
33
|
-
"""
|
|
34
|
-
Resolve a board by its ID.
|
|
35
|
-
|
|
36
|
-
Checks authorization: board must be public or user must be owner/member.
|
|
37
|
-
"""
|
|
38
|
-
auth_context = await get_auth_context_from_info(info)
|
|
39
|
-
if auth_context is None:
|
|
40
|
-
return None
|
|
41
|
-
|
|
42
|
-
async with get_async_session() as session:
|
|
43
|
-
# Query board with owner and members eagerly loaded
|
|
44
|
-
stmt = (
|
|
45
|
-
select(Boards)
|
|
46
|
-
.where(Boards.id == id)
|
|
47
|
-
.options(
|
|
48
|
-
selectinload(Boards.owner),
|
|
49
|
-
selectinload(Boards.board_members).selectinload(BoardMembers.user),
|
|
50
|
-
)
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
result = await session.execute(stmt)
|
|
54
|
-
board = result.scalar_one_or_none()
|
|
55
|
-
|
|
56
|
-
if not board:
|
|
57
|
-
logger.info("Board not found", board_id=str(id))
|
|
58
|
-
return None
|
|
59
|
-
|
|
60
|
-
# Check authorization using shared logic
|
|
61
|
-
if not can_access_board(board, auth_context):
|
|
62
|
-
logger.info(
|
|
63
|
-
"Access denied to board",
|
|
64
|
-
board_id=str(id),
|
|
65
|
-
user_id=(
|
|
66
|
-
str(auth_context.user_id) if auth_context and auth_context.user_id else None
|
|
67
|
-
),
|
|
68
|
-
)
|
|
69
|
-
return None
|
|
70
|
-
|
|
71
|
-
# Convert SQLAlchemy model to GraphQL type
|
|
72
|
-
from ..types.board import Board as BoardType
|
|
73
|
-
|
|
74
|
-
return BoardType(
|
|
75
|
-
id=board.id,
|
|
76
|
-
tenant_id=board.tenant_id,
|
|
77
|
-
owner_id=board.owner_id,
|
|
78
|
-
title=board.title,
|
|
79
|
-
description=board.description,
|
|
80
|
-
is_public=board.is_public,
|
|
81
|
-
settings=board.settings or {},
|
|
82
|
-
metadata=board.metadata_ or {},
|
|
83
|
-
created_at=board.created_at,
|
|
84
|
-
updated_at=board.updated_at,
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
async def resolve_my_boards(
|
|
89
|
-
info: strawberry.Info,
|
|
90
|
-
limit: int,
|
|
91
|
-
offset: int,
|
|
92
|
-
role: BoardQueryRole = BoardQueryRole.ANY,
|
|
93
|
-
sort: SortOrder = SortOrder.UPDATED_DESC,
|
|
94
|
-
) -> list[Board]:
|
|
95
|
-
"""
|
|
96
|
-
Resolve boards where the authenticated user is owner or member.
|
|
97
|
-
|
|
98
|
-
Args:
|
|
99
|
-
role: Filter by role (ANY, OWNER, MEMBER)
|
|
100
|
-
sort: Sort order for results
|
|
101
|
-
"""
|
|
102
|
-
auth_context = await get_auth_context_from_info(info)
|
|
103
|
-
if not auth_context or not auth_context.is_authenticated:
|
|
104
|
-
logger.info("Unauthenticated access to my_boards")
|
|
105
|
-
return []
|
|
106
|
-
|
|
107
|
-
async with get_async_session() as session:
|
|
108
|
-
# Build the query based on role filter
|
|
109
|
-
if role == BoardQueryRole.OWNER:
|
|
110
|
-
# Only boards owned by user
|
|
111
|
-
boards_condition = Boards.owner_id == auth_context.user_id
|
|
112
|
-
elif role == BoardQueryRole.MEMBER:
|
|
113
|
-
# Only boards where user is a member (not owner)
|
|
114
|
-
member_board_ids = select(BoardMembers.board_id).where(
|
|
115
|
-
BoardMembers.user_id == auth_context.user_id
|
|
116
|
-
)
|
|
117
|
-
boards_condition = and_(
|
|
118
|
-
Boards.id.in_(member_board_ids), Boards.owner_id != auth_context.user_id
|
|
119
|
-
)
|
|
120
|
-
else: # BoardQueryRole.ANY
|
|
121
|
-
# Boards where user is owner OR member
|
|
122
|
-
member_board_ids = select(BoardMembers.board_id).where(
|
|
123
|
-
BoardMembers.user_id == auth_context.user_id
|
|
124
|
-
)
|
|
125
|
-
boards_condition = or_(
|
|
126
|
-
Boards.owner_id == auth_context.user_id, Boards.id.in_(member_board_ids)
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
# Add sorting
|
|
130
|
-
if sort == SortOrder.CREATED_ASC:
|
|
131
|
-
order_by = Boards.created_at.asc()
|
|
132
|
-
elif sort == SortOrder.CREATED_DESC:
|
|
133
|
-
order_by = Boards.created_at.desc()
|
|
134
|
-
elif sort == SortOrder.UPDATED_ASC:
|
|
135
|
-
order_by = Boards.updated_at.asc()
|
|
136
|
-
else: # UPDATED_DESC (default)
|
|
137
|
-
order_by = Boards.updated_at.desc()
|
|
138
|
-
|
|
139
|
-
stmt = (
|
|
140
|
-
select(Boards)
|
|
141
|
-
.where(boards_condition)
|
|
142
|
-
.options(
|
|
143
|
-
selectinload(Boards.owner),
|
|
144
|
-
selectinload(Boards.board_members).selectinload(BoardMembers.user),
|
|
145
|
-
)
|
|
146
|
-
.order_by(order_by)
|
|
147
|
-
.limit(limit)
|
|
148
|
-
.offset(offset)
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
result = await session.execute(stmt)
|
|
152
|
-
boards = result.scalars().all()
|
|
153
|
-
|
|
154
|
-
# Convert to GraphQL types
|
|
155
|
-
from ..types.board import Board as BoardType
|
|
156
|
-
|
|
157
|
-
return [
|
|
158
|
-
BoardType(
|
|
159
|
-
id=board.id,
|
|
160
|
-
tenant_id=board.tenant_id,
|
|
161
|
-
owner_id=board.owner_id,
|
|
162
|
-
title=board.title,
|
|
163
|
-
description=board.description,
|
|
164
|
-
is_public=board.is_public,
|
|
165
|
-
settings=board.settings or {},
|
|
166
|
-
metadata=board.metadata_ or {},
|
|
167
|
-
created_at=board.created_at,
|
|
168
|
-
updated_at=board.updated_at,
|
|
169
|
-
)
|
|
170
|
-
for board in boards
|
|
171
|
-
]
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
async def resolve_public_boards(
|
|
175
|
-
info: strawberry.Info,
|
|
176
|
-
limit: int,
|
|
177
|
-
offset: int,
|
|
178
|
-
sort: SortOrder = SortOrder.UPDATED_DESC,
|
|
179
|
-
) -> list[Board]:
|
|
180
|
-
"""
|
|
181
|
-
Resolve public boards (no authentication required).
|
|
182
|
-
|
|
183
|
-
Args:
|
|
184
|
-
sort: Sort order for results
|
|
185
|
-
"""
|
|
186
|
-
async with get_async_session() as session:
|
|
187
|
-
# Add sorting
|
|
188
|
-
if sort == SortOrder.CREATED_ASC:
|
|
189
|
-
order_by = Boards.created_at.asc()
|
|
190
|
-
elif sort == SortOrder.CREATED_DESC:
|
|
191
|
-
order_by = Boards.created_at.desc()
|
|
192
|
-
elif sort == SortOrder.UPDATED_ASC:
|
|
193
|
-
order_by = Boards.updated_at.asc()
|
|
194
|
-
else: # UPDATED_DESC (default)
|
|
195
|
-
order_by = Boards.updated_at.desc()
|
|
196
|
-
|
|
197
|
-
stmt = (
|
|
198
|
-
select(Boards)
|
|
199
|
-
.where(Boards.is_public)
|
|
200
|
-
.options(
|
|
201
|
-
selectinload(Boards.owner),
|
|
202
|
-
selectinload(Boards.board_members).selectinload(BoardMembers.user),
|
|
203
|
-
)
|
|
204
|
-
.order_by(order_by)
|
|
205
|
-
.limit(limit)
|
|
206
|
-
.offset(offset)
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
result = await session.execute(stmt)
|
|
210
|
-
boards = result.scalars().all()
|
|
211
|
-
|
|
212
|
-
# Convert to GraphQL types
|
|
213
|
-
from ..types.board import Board as BoardType
|
|
214
|
-
|
|
215
|
-
return [
|
|
216
|
-
BoardType(
|
|
217
|
-
id=board.id,
|
|
218
|
-
tenant_id=board.tenant_id,
|
|
219
|
-
owner_id=board.owner_id,
|
|
220
|
-
title=board.title,
|
|
221
|
-
description=board.description,
|
|
222
|
-
is_public=board.is_public,
|
|
223
|
-
settings=board.settings or {},
|
|
224
|
-
metadata=board.metadata_ or {},
|
|
225
|
-
created_at=board.created_at,
|
|
226
|
-
updated_at=board.updated_at,
|
|
227
|
-
)
|
|
228
|
-
for board in boards
|
|
229
|
-
]
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
async def search_boards(info: strawberry.Info, query: str, limit: int, offset: int) -> list[Board]:
|
|
233
|
-
"""
|
|
234
|
-
Search for boards based on a text query.
|
|
235
|
-
|
|
236
|
-
Searches board titles and descriptions for the query string.
|
|
237
|
-
Only returns boards the user has access to.
|
|
238
|
-
"""
|
|
239
|
-
auth_context = await get_auth_context_from_info(info)
|
|
240
|
-
|
|
241
|
-
async with get_async_session() as session:
|
|
242
|
-
# Build base query with case-insensitive search
|
|
243
|
-
search_pattern = f"%{query}%"
|
|
244
|
-
|
|
245
|
-
# Base condition for text search
|
|
246
|
-
search_condition = or_(
|
|
247
|
-
Boards.title.ilike(search_pattern), Boards.description.ilike(search_pattern)
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
# Add access control conditions
|
|
251
|
-
if auth_context and auth_context.is_authenticated:
|
|
252
|
-
# User can see: public boards OR boards they own OR boards they're a member of
|
|
253
|
-
member_board_ids = select(BoardMembers.board_id).where(
|
|
254
|
-
BoardMembers.user_id == auth_context.user_id
|
|
255
|
-
)
|
|
256
|
-
access_condition = or_(
|
|
257
|
-
Boards.is_public,
|
|
258
|
-
Boards.owner_id == auth_context.user_id,
|
|
259
|
-
Boards.id.in_(member_board_ids),
|
|
260
|
-
)
|
|
261
|
-
else:
|
|
262
|
-
# Unauthenticated users can only see public boards
|
|
263
|
-
access_condition = Boards.is_public
|
|
264
|
-
|
|
265
|
-
stmt = (
|
|
266
|
-
select(Boards)
|
|
267
|
-
.where(and_(search_condition, access_condition))
|
|
268
|
-
.options(
|
|
269
|
-
selectinload(Boards.owner),
|
|
270
|
-
selectinload(Boards.board_members).selectinload(BoardMembers.user),
|
|
271
|
-
)
|
|
272
|
-
.order_by(Boards.updated_at.desc())
|
|
273
|
-
.limit(limit)
|
|
274
|
-
.offset(offset)
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
result = await session.execute(stmt)
|
|
278
|
-
boards = result.scalars().all()
|
|
279
|
-
|
|
280
|
-
# Convert to GraphQL types
|
|
281
|
-
from ..types.board import Board as BoardType
|
|
282
|
-
|
|
283
|
-
return [
|
|
284
|
-
BoardType(
|
|
285
|
-
id=board.id,
|
|
286
|
-
tenant_id=board.tenant_id,
|
|
287
|
-
owner_id=board.owner_id,
|
|
288
|
-
title=board.title,
|
|
289
|
-
description=board.description,
|
|
290
|
-
is_public=board.is_public,
|
|
291
|
-
settings=board.settings or {},
|
|
292
|
-
metadata=board.metadata_ or {},
|
|
293
|
-
created_at=board.created_at,
|
|
294
|
-
updated_at=board.updated_at,
|
|
295
|
-
)
|
|
296
|
-
for board in boards
|
|
297
|
-
]
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
# Board field resolvers
|
|
301
|
-
async def resolve_board_owner(board: Board, info: strawberry.Info) -> User:
|
|
302
|
-
"""
|
|
303
|
-
Resolve the owner of a board. Requires user to have access to board details.
|
|
304
|
-
"""
|
|
305
|
-
auth_context = await get_auth_context_from_info(info)
|
|
306
|
-
|
|
307
|
-
# Check if user can access board details
|
|
308
|
-
# We need to get the actual board from database to check access
|
|
309
|
-
async with get_async_session() as session:
|
|
310
|
-
stmt = (
|
|
311
|
-
select(Boards)
|
|
312
|
-
.where(Boards.id == board.id)
|
|
313
|
-
.options(
|
|
314
|
-
selectinload(Boards.owner),
|
|
315
|
-
selectinload(Boards.board_members),
|
|
316
|
-
)
|
|
317
|
-
)
|
|
318
|
-
result = await session.execute(stmt)
|
|
319
|
-
db_board = result.scalar_one_or_none()
|
|
320
|
-
|
|
321
|
-
if not db_board or not can_access_board_details(db_board, auth_context):
|
|
322
|
-
raise RuntimeError("Access denied to board owner information")
|
|
323
|
-
|
|
324
|
-
# Ensure owner is preloaded
|
|
325
|
-
ensure_preloaded(db_board, "owner", "Board owner relationship was not preloaded")
|
|
326
|
-
|
|
327
|
-
if not db_board.owner:
|
|
328
|
-
raise RuntimeError("Board owner not found")
|
|
329
|
-
|
|
330
|
-
from ..types.user import User as UserType
|
|
331
|
-
|
|
332
|
-
return UserType(
|
|
333
|
-
id=db_board.owner.id,
|
|
334
|
-
tenant_id=db_board.owner.tenant_id,
|
|
335
|
-
auth_provider=db_board.owner.auth_provider,
|
|
336
|
-
auth_subject=db_board.owner.auth_subject,
|
|
337
|
-
email=db_board.owner.email,
|
|
338
|
-
display_name=db_board.owner.display_name,
|
|
339
|
-
avatar_url=db_board.owner.avatar_url,
|
|
340
|
-
created_at=db_board.owner.created_at,
|
|
341
|
-
updated_at=db_board.owner.updated_at,
|
|
342
|
-
)
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
async def resolve_board_members(board: Board, info: strawberry.Info) -> list[BoardMember]:
|
|
346
|
-
"""
|
|
347
|
-
Resolve the members of a board. Requires user to have access to board details.
|
|
348
|
-
"""
|
|
349
|
-
auth_context = await get_auth_context_from_info(info)
|
|
350
|
-
|
|
351
|
-
# Check if user can access board details
|
|
352
|
-
async with get_async_session() as session:
|
|
353
|
-
stmt = (
|
|
354
|
-
select(Boards)
|
|
355
|
-
.where(Boards.id == board.id)
|
|
356
|
-
.options(
|
|
357
|
-
selectinload(Boards.board_members).selectinload(BoardMembers.user),
|
|
358
|
-
selectinload(Boards.owner),
|
|
359
|
-
)
|
|
360
|
-
)
|
|
361
|
-
result = await session.execute(stmt)
|
|
362
|
-
db_board = result.scalar_one_or_none()
|
|
363
|
-
|
|
364
|
-
if not db_board or not can_access_board_details(db_board, auth_context):
|
|
365
|
-
raise RuntimeError("Access denied to board member information")
|
|
366
|
-
|
|
367
|
-
# Ensure members are preloaded
|
|
368
|
-
ensure_preloaded(db_board, "board_members", "Board members relationship was not preloaded")
|
|
369
|
-
|
|
370
|
-
from ..types.board import BoardMember as BoardMemberType
|
|
371
|
-
from ..types.board import BoardRole
|
|
372
|
-
|
|
373
|
-
members = []
|
|
374
|
-
for member in db_board.board_members:
|
|
375
|
-
# Ensure user relationship is preloaded
|
|
376
|
-
ensure_preloaded(member, "user", "BoardMember user relationship was not preloaded")
|
|
377
|
-
|
|
378
|
-
members.append(
|
|
379
|
-
BoardMemberType(
|
|
380
|
-
id=member.id,
|
|
381
|
-
board_id=member.board_id,
|
|
382
|
-
user_id=member.user_id,
|
|
383
|
-
role=BoardRole(member.role),
|
|
384
|
-
invited_by=member.invited_by,
|
|
385
|
-
joined_at=member.joined_at,
|
|
386
|
-
)
|
|
387
|
-
)
|
|
388
|
-
|
|
389
|
-
return members
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
async def resolve_board_generations(
|
|
393
|
-
board: Board, info: strawberry.Info, limit: int, offset: int
|
|
394
|
-
) -> list[Generation]:
|
|
395
|
-
"""
|
|
396
|
-
Resolve generations for a board. Requires user to have access to the board.
|
|
397
|
-
"""
|
|
398
|
-
auth_context = await get_auth_context_from_info(info)
|
|
399
|
-
|
|
400
|
-
# Check if user can access board
|
|
401
|
-
async with get_async_session() as session:
|
|
402
|
-
# First check board access
|
|
403
|
-
stmt = (
|
|
404
|
-
select(Boards)
|
|
405
|
-
.where(Boards.id == board.id)
|
|
406
|
-
.options(
|
|
407
|
-
selectinload(Boards.board_members),
|
|
408
|
-
)
|
|
409
|
-
)
|
|
410
|
-
result = await session.execute(stmt)
|
|
411
|
-
db_board = result.scalar_one_or_none()
|
|
412
|
-
|
|
413
|
-
if not db_board or not can_access_board(db_board, auth_context):
|
|
414
|
-
logger.info("Access denied to board generations", board_id=str(board.id))
|
|
415
|
-
return []
|
|
416
|
-
|
|
417
|
-
# Query generations for this board
|
|
418
|
-
generations_stmt = (
|
|
419
|
-
select(Generations)
|
|
420
|
-
.where(Generations.board_id == board.id)
|
|
421
|
-
.order_by(Generations.created_at.desc())
|
|
422
|
-
.limit(limit)
|
|
423
|
-
.offset(offset)
|
|
424
|
-
)
|
|
425
|
-
|
|
426
|
-
generations_result = await session.execute(generations_stmt)
|
|
427
|
-
generations = generations_result.scalars().all()
|
|
428
|
-
|
|
429
|
-
from ..types.generation import ArtifactType, GenerationStatus
|
|
430
|
-
from ..types.generation import Generation as GenerationType
|
|
431
|
-
|
|
432
|
-
return [
|
|
433
|
-
GenerationType(
|
|
434
|
-
id=gen.id,
|
|
435
|
-
tenant_id=gen.tenant_id,
|
|
436
|
-
board_id=gen.board_id,
|
|
437
|
-
user_id=gen.user_id,
|
|
438
|
-
generator_name=gen.generator_name,
|
|
439
|
-
artifact_type=ArtifactType(gen.artifact_type),
|
|
440
|
-
storage_url=gen.storage_url,
|
|
441
|
-
thumbnail_url=gen.thumbnail_url,
|
|
442
|
-
additional_files=gen.additional_files or [],
|
|
443
|
-
input_params=gen.input_params or {},
|
|
444
|
-
output_metadata=gen.output_metadata or {},
|
|
445
|
-
external_job_id=gen.external_job_id,
|
|
446
|
-
status=GenerationStatus(gen.status),
|
|
447
|
-
progress=float(gen.progress or 0.0),
|
|
448
|
-
error_message=gen.error_message,
|
|
449
|
-
started_at=gen.started_at,
|
|
450
|
-
completed_at=gen.completed_at,
|
|
451
|
-
created_at=gen.created_at,
|
|
452
|
-
updated_at=gen.updated_at,
|
|
453
|
-
)
|
|
454
|
-
for gen in generations
|
|
455
|
-
]
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
async def resolve_board_generation_count(board: Board, info: strawberry.Info) -> int:
|
|
459
|
-
"""
|
|
460
|
-
Get the total count of generations for a board.
|
|
461
|
-
|
|
462
|
-
More efficient than fetching all generations when only count is needed.
|
|
463
|
-
"""
|
|
464
|
-
auth_context = await get_auth_context_from_info(info)
|
|
465
|
-
|
|
466
|
-
async with get_async_session() as session:
|
|
467
|
-
# First check board access
|
|
468
|
-
stmt = (
|
|
469
|
-
select(Boards)
|
|
470
|
-
.where(Boards.id == board.id)
|
|
471
|
-
.options(
|
|
472
|
-
selectinload(Boards.board_members),
|
|
473
|
-
)
|
|
474
|
-
)
|
|
475
|
-
result = await session.execute(stmt)
|
|
476
|
-
db_board = result.scalar_one_or_none()
|
|
477
|
-
|
|
478
|
-
if not db_board or not can_access_board(db_board, auth_context):
|
|
479
|
-
logger.info("Access denied to board generation count", board_id=str(board.id))
|
|
480
|
-
return 0
|
|
481
|
-
|
|
482
|
-
# Count generations for this board
|
|
483
|
-
from sqlalchemy import func
|
|
484
|
-
|
|
485
|
-
count_stmt = select(func.count(Generations.id)).where(Generations.board_id == board.id)
|
|
486
|
-
|
|
487
|
-
count_result = await session.execute(count_stmt)
|
|
488
|
-
return count_result.scalar() or 0
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
# BoardMember field resolvers
|
|
492
|
-
async def resolve_board_member_user(member: BoardMember, info: strawberry.Info) -> User:
|
|
493
|
-
"""
|
|
494
|
-
Resolve the user for a board member. Requires access to board details.
|
|
495
|
-
"""
|
|
496
|
-
auth_context = await get_auth_context_from_info(info)
|
|
497
|
-
|
|
498
|
-
async with get_async_session() as session:
|
|
499
|
-
# First verify access to the board that this member belongs to
|
|
500
|
-
board_stmt = (
|
|
501
|
-
select(Boards)
|
|
502
|
-
.where(Boards.id == member.board_id)
|
|
503
|
-
.options(
|
|
504
|
-
selectinload(Boards.board_members),
|
|
505
|
-
)
|
|
506
|
-
)
|
|
507
|
-
board_result = await session.execute(board_stmt)
|
|
508
|
-
board = board_result.scalar_one_or_none()
|
|
509
|
-
|
|
510
|
-
if not board or not can_access_board_details(board, auth_context):
|
|
511
|
-
raise RuntimeError("Access denied to board member information")
|
|
512
|
-
|
|
513
|
-
# Query the user
|
|
514
|
-
user_stmt = select(Users).where(Users.id == member.user_id)
|
|
515
|
-
user_result = await session.execute(user_stmt)
|
|
516
|
-
user = user_result.scalar_one_or_none()
|
|
517
|
-
|
|
518
|
-
if not user:
|
|
519
|
-
raise RuntimeError("Board member user not found")
|
|
520
|
-
|
|
521
|
-
from ..types.user import User as UserType
|
|
522
|
-
|
|
523
|
-
return UserType(
|
|
524
|
-
id=user.id,
|
|
525
|
-
tenant_id=user.tenant_id,
|
|
526
|
-
auth_provider=user.auth_provider,
|
|
527
|
-
auth_subject=user.auth_subject,
|
|
528
|
-
email=user.email,
|
|
529
|
-
display_name=user.display_name,
|
|
530
|
-
avatar_url=user.avatar_url,
|
|
531
|
-
created_at=user.created_at,
|
|
532
|
-
updated_at=user.updated_at,
|
|
533
|
-
)
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
async def resolve_board_member_inviter(member: BoardMember, info: strawberry.Info) -> User | None:
|
|
537
|
-
"""
|
|
538
|
-
Resolve the user who invited this board member.
|
|
539
|
-
|
|
540
|
-
Returns None if the member is the original owner or if no inviter is recorded.
|
|
541
|
-
"""
|
|
542
|
-
if not member.invited_by:
|
|
543
|
-
return None
|
|
544
|
-
|
|
545
|
-
auth_context = await get_auth_context_from_info(info)
|
|
546
|
-
|
|
547
|
-
async with get_async_session() as session:
|
|
548
|
-
# First verify access to the board that this member belongs to
|
|
549
|
-
board_stmt = (
|
|
550
|
-
select(Boards)
|
|
551
|
-
.where(Boards.id == member.board_id)
|
|
552
|
-
.options(
|
|
553
|
-
selectinload(Boards.board_members),
|
|
554
|
-
)
|
|
555
|
-
)
|
|
556
|
-
board_result = await session.execute(board_stmt)
|
|
557
|
-
board = board_result.scalar_one_or_none()
|
|
558
|
-
|
|
559
|
-
if not board or not can_access_board_details(board, auth_context):
|
|
560
|
-
raise RuntimeError("Access denied to board member inviter information")
|
|
561
|
-
|
|
562
|
-
# Query the inviter
|
|
563
|
-
inviter_stmt = select(Users).where(Users.id == member.invited_by)
|
|
564
|
-
inviter_result = await session.execute(inviter_stmt)
|
|
565
|
-
inviter = inviter_result.scalar_one_or_none()
|
|
566
|
-
|
|
567
|
-
if not inviter:
|
|
568
|
-
return None
|
|
569
|
-
|
|
570
|
-
from ..types.user import User as UserType
|
|
571
|
-
|
|
572
|
-
return UserType(
|
|
573
|
-
id=inviter.id,
|
|
574
|
-
tenant_id=inviter.tenant_id,
|
|
575
|
-
auth_provider=inviter.auth_provider,
|
|
576
|
-
auth_subject=inviter.auth_subject,
|
|
577
|
-
email=inviter.email,
|
|
578
|
-
display_name=inviter.display_name,
|
|
579
|
-
avatar_url=inviter.avatar_url,
|
|
580
|
-
created_at=inviter.created_at,
|
|
581
|
-
updated_at=inviter.updated_at,
|
|
582
|
-
)
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
# Mutation resolvers
|
|
586
|
-
async def create_board(info: strawberry.Info, input: CreateBoardInput) -> Board:
|
|
587
|
-
"""
|
|
588
|
-
Create a new board.
|
|
589
|
-
|
|
590
|
-
The authenticated user becomes the owner of the board.
|
|
591
|
-
"""
|
|
592
|
-
auth_context = await get_auth_context_from_info(info)
|
|
593
|
-
if not auth_context or not auth_context.is_authenticated:
|
|
594
|
-
raise RuntimeError("Authentication required to create a board")
|
|
595
|
-
|
|
596
|
-
async with get_async_session() as session:
|
|
597
|
-
# Get the tenant UUID from the database
|
|
598
|
-
tenant_uuid = auth_context.tenant_id
|
|
599
|
-
|
|
600
|
-
# Create the new board
|
|
601
|
-
new_board = Boards(
|
|
602
|
-
tenant_id=tenant_uuid,
|
|
603
|
-
owner_id=auth_context.user_id,
|
|
604
|
-
title=input.title,
|
|
605
|
-
description=input.description,
|
|
606
|
-
is_public=input.is_public,
|
|
607
|
-
settings=input.settings or {},
|
|
608
|
-
)
|
|
609
|
-
|
|
610
|
-
session.add(new_board)
|
|
611
|
-
await session.commit()
|
|
612
|
-
await session.refresh(new_board)
|
|
613
|
-
|
|
614
|
-
# Load relationships for the response
|
|
615
|
-
stmt = (
|
|
616
|
-
select(Boards)
|
|
617
|
-
.where(Boards.id == new_board.id)
|
|
618
|
-
.options(
|
|
619
|
-
selectinload(Boards.owner),
|
|
620
|
-
selectinload(Boards.board_members).selectinload(BoardMembers.user),
|
|
621
|
-
)
|
|
622
|
-
)
|
|
623
|
-
result = await session.execute(stmt)
|
|
624
|
-
board = result.scalar_one()
|
|
625
|
-
|
|
626
|
-
logger.info(
|
|
627
|
-
"Board created",
|
|
628
|
-
board_id=str(board.id),
|
|
629
|
-
user_id=str(auth_context.user_id),
|
|
630
|
-
title=board.title,
|
|
631
|
-
)
|
|
632
|
-
|
|
633
|
-
from ..types.board import Board as BoardType
|
|
634
|
-
|
|
635
|
-
return BoardType(
|
|
636
|
-
id=board.id,
|
|
637
|
-
tenant_id=tenant_uuid,
|
|
638
|
-
owner_id=board.owner_id,
|
|
639
|
-
title=board.title,
|
|
640
|
-
description=board.description,
|
|
641
|
-
is_public=board.is_public,
|
|
642
|
-
settings=board.settings or {},
|
|
643
|
-
metadata=board.metadata_ or {},
|
|
644
|
-
created_at=board.created_at,
|
|
645
|
-
updated_at=board.updated_at,
|
|
646
|
-
)
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
async def update_board(info: strawberry.Info, input: UpdateBoardInput) -> Board:
|
|
650
|
-
"""
|
|
651
|
-
Update an existing board.
|
|
652
|
-
|
|
653
|
-
Only the board owner or an admin member can update the board.
|
|
654
|
-
"""
|
|
655
|
-
auth_context = await get_auth_context_from_info(info)
|
|
656
|
-
if not auth_context or not auth_context.is_authenticated:
|
|
657
|
-
raise RuntimeError("Authentication required to update a board")
|
|
658
|
-
|
|
659
|
-
async with get_async_session() as session:
|
|
660
|
-
# Get the board with members
|
|
661
|
-
stmt = (
|
|
662
|
-
select(Boards)
|
|
663
|
-
.where(Boards.id == input.id)
|
|
664
|
-
.options(
|
|
665
|
-
selectinload(Boards.owner),
|
|
666
|
-
selectinload(Boards.board_members).selectinload(BoardMembers.user),
|
|
667
|
-
)
|
|
668
|
-
)
|
|
669
|
-
result = await session.execute(stmt)
|
|
670
|
-
board = result.scalar_one_or_none()
|
|
671
|
-
|
|
672
|
-
if not board:
|
|
673
|
-
raise RuntimeError("Board not found")
|
|
674
|
-
|
|
675
|
-
# Check permissions: must be owner or admin
|
|
676
|
-
is_owner = board.owner_id == auth_context.user_id
|
|
677
|
-
is_admin = any(
|
|
678
|
-
member.user_id == auth_context.user_id and member.role == "admin"
|
|
679
|
-
for member in board.board_members
|
|
680
|
-
)
|
|
681
|
-
|
|
682
|
-
if not is_owner and not is_admin:
|
|
683
|
-
raise RuntimeError("Permission denied: only board owner or admin can update")
|
|
684
|
-
|
|
685
|
-
# Update fields if provided
|
|
686
|
-
if input.title is not None:
|
|
687
|
-
board.title = input.title
|
|
688
|
-
if input.description is not None:
|
|
689
|
-
board.description = input.description
|
|
690
|
-
if input.is_public is not None:
|
|
691
|
-
board.is_public = input.is_public
|
|
692
|
-
if input.settings is not None:
|
|
693
|
-
board.settings = input.settings
|
|
694
|
-
|
|
695
|
-
await session.commit()
|
|
696
|
-
await session.refresh(board)
|
|
697
|
-
|
|
698
|
-
logger.info(
|
|
699
|
-
"Board updated",
|
|
700
|
-
board_id=str(board.id),
|
|
701
|
-
user_id=str(auth_context.user_id),
|
|
702
|
-
updated_fields=[
|
|
703
|
-
k
|
|
704
|
-
for k, v in {
|
|
705
|
-
"title": input.title,
|
|
706
|
-
"description": input.description,
|
|
707
|
-
"is_public": input.is_public,
|
|
708
|
-
"settings": input.settings,
|
|
709
|
-
}.items()
|
|
710
|
-
if v is not None
|
|
711
|
-
],
|
|
712
|
-
)
|
|
713
|
-
|
|
714
|
-
from ..types.board import Board as BoardType
|
|
715
|
-
|
|
716
|
-
return BoardType(
|
|
717
|
-
id=board.id,
|
|
718
|
-
tenant_id=board.tenant_id,
|
|
719
|
-
owner_id=board.owner_id,
|
|
720
|
-
title=board.title,
|
|
721
|
-
description=board.description,
|
|
722
|
-
is_public=board.is_public,
|
|
723
|
-
settings=board.settings or {},
|
|
724
|
-
metadata=board.metadata_ or {},
|
|
725
|
-
created_at=board.created_at,
|
|
726
|
-
updated_at=board.updated_at,
|
|
727
|
-
)
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
async def delete_board(info: strawberry.Info, id: UUID) -> bool:
|
|
731
|
-
"""
|
|
732
|
-
Delete a board and all associated data.
|
|
733
|
-
|
|
734
|
-
Only the board owner can delete a board.
|
|
735
|
-
"""
|
|
736
|
-
auth_context = await get_auth_context_from_info(info)
|
|
737
|
-
if not auth_context or not auth_context.is_authenticated:
|
|
738
|
-
raise RuntimeError("Authentication required to delete a board")
|
|
739
|
-
|
|
740
|
-
async with get_async_session() as session:
|
|
741
|
-
# Get the board
|
|
742
|
-
stmt = select(Boards).where(Boards.id == id)
|
|
743
|
-
result = await session.execute(stmt)
|
|
744
|
-
board = result.scalar_one_or_none()
|
|
745
|
-
|
|
746
|
-
if not board:
|
|
747
|
-
raise RuntimeError("Board not found")
|
|
748
|
-
|
|
749
|
-
# Check if user is the owner
|
|
750
|
-
if board.owner_id != auth_context.user_id:
|
|
751
|
-
raise RuntimeError("Permission denied: only board owner can delete")
|
|
752
|
-
|
|
753
|
-
# Delete the board (cascade will handle related records)
|
|
754
|
-
await session.delete(board)
|
|
755
|
-
await session.commit()
|
|
756
|
-
|
|
757
|
-
logger.info("Board deleted", board_id=str(id), user_id=str(auth_context.user_id))
|
|
758
|
-
|
|
759
|
-
return True
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
async def add_board_member(info: strawberry.Info, input: AddBoardMemberInput) -> Board:
|
|
763
|
-
"""
|
|
764
|
-
Add a new member to a board.
|
|
765
|
-
|
|
766
|
-
Only the board owner or an admin member can add new members.
|
|
767
|
-
"""
|
|
768
|
-
auth_context = await get_auth_context_from_info(info)
|
|
769
|
-
if not auth_context or not auth_context.is_authenticated:
|
|
770
|
-
raise RuntimeError("Authentication required to add board members")
|
|
771
|
-
|
|
772
|
-
async with get_async_session() as session:
|
|
773
|
-
# Get the board with members
|
|
774
|
-
stmt = (
|
|
775
|
-
select(Boards)
|
|
776
|
-
.where(Boards.id == input.board_id)
|
|
777
|
-
.options(
|
|
778
|
-
selectinload(Boards.owner),
|
|
779
|
-
selectinload(Boards.board_members).selectinload(BoardMembers.user),
|
|
780
|
-
)
|
|
781
|
-
)
|
|
782
|
-
result = await session.execute(stmt)
|
|
783
|
-
board = result.scalar_one_or_none()
|
|
784
|
-
|
|
785
|
-
if not board:
|
|
786
|
-
raise RuntimeError("Board not found")
|
|
787
|
-
|
|
788
|
-
# Check permissions: must be owner or admin
|
|
789
|
-
is_owner = board.owner_id == auth_context.user_id
|
|
790
|
-
is_admin = any(
|
|
791
|
-
member.user_id == auth_context.user_id and member.role == "admin"
|
|
792
|
-
for member in board.board_members
|
|
793
|
-
)
|
|
794
|
-
|
|
795
|
-
if not is_owner and not is_admin:
|
|
796
|
-
raise RuntimeError("Permission denied: only board owner or admin can add members")
|
|
797
|
-
|
|
798
|
-
# Check if user to be added exists
|
|
799
|
-
user_stmt = select(Users).where(Users.id == input.user_id)
|
|
800
|
-
user_result = await session.execute(user_stmt)
|
|
801
|
-
user = user_result.scalar_one_or_none()
|
|
802
|
-
|
|
803
|
-
if not user:
|
|
804
|
-
raise RuntimeError("User not found")
|
|
805
|
-
|
|
806
|
-
# Check if user is already the owner
|
|
807
|
-
if board.owner_id == input.user_id:
|
|
808
|
-
raise RuntimeError("User is already the board owner")
|
|
809
|
-
|
|
810
|
-
# Check if user is already a member
|
|
811
|
-
existing_member = any(member.user_id == input.user_id for member in board.board_members)
|
|
812
|
-
|
|
813
|
-
if existing_member:
|
|
814
|
-
raise RuntimeError("User is already a board member")
|
|
815
|
-
|
|
816
|
-
# Add the new member
|
|
817
|
-
new_member = BoardMembers(
|
|
818
|
-
board_id=input.board_id,
|
|
819
|
-
user_id=input.user_id,
|
|
820
|
-
role=input.role.value,
|
|
821
|
-
invited_by=auth_context.user_id,
|
|
822
|
-
)
|
|
823
|
-
|
|
824
|
-
session.add(new_member)
|
|
825
|
-
await session.commit()
|
|
826
|
-
|
|
827
|
-
# Refresh the board to get updated members
|
|
828
|
-
await session.refresh(board)
|
|
829
|
-
|
|
830
|
-
# Re-query with all relationships loaded
|
|
831
|
-
stmt = (
|
|
832
|
-
select(Boards)
|
|
833
|
-
.where(Boards.id == input.board_id)
|
|
834
|
-
.options(
|
|
835
|
-
selectinload(Boards.owner),
|
|
836
|
-
selectinload(Boards.board_members).selectinload(BoardMembers.user),
|
|
837
|
-
)
|
|
838
|
-
)
|
|
839
|
-
result = await session.execute(stmt)
|
|
840
|
-
board = result.scalar_one()
|
|
841
|
-
|
|
842
|
-
logger.info(
|
|
843
|
-
"Board member added",
|
|
844
|
-
board_id=str(board.id),
|
|
845
|
-
user_id=str(input.user_id),
|
|
846
|
-
role=input.role.value,
|
|
847
|
-
invited_by=str(auth_context.user_id),
|
|
848
|
-
)
|
|
849
|
-
|
|
850
|
-
from ..types.board import Board as BoardType
|
|
851
|
-
|
|
852
|
-
return BoardType(
|
|
853
|
-
id=board.id,
|
|
854
|
-
tenant_id=board.tenant_id,
|
|
855
|
-
owner_id=board.owner_id,
|
|
856
|
-
title=board.title,
|
|
857
|
-
description=board.description,
|
|
858
|
-
is_public=board.is_public,
|
|
859
|
-
settings=board.settings or {},
|
|
860
|
-
metadata=board.metadata_ or {},
|
|
861
|
-
created_at=board.created_at,
|
|
862
|
-
updated_at=board.updated_at,
|
|
863
|
-
)
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
async def remove_board_member(info: strawberry.Info, board_id: UUID, user_id: UUID) -> Board:
|
|
867
|
-
"""
|
|
868
|
-
Remove a member from a board.
|
|
869
|
-
|
|
870
|
-
Only the board owner, an admin member, or the member themselves can remove a member.
|
|
871
|
-
The board owner cannot be removed.
|
|
872
|
-
"""
|
|
873
|
-
auth_context = await get_auth_context_from_info(info)
|
|
874
|
-
if not auth_context or not auth_context.is_authenticated:
|
|
875
|
-
raise RuntimeError("Authentication required to remove board members")
|
|
876
|
-
|
|
877
|
-
async with get_async_session() as session:
|
|
878
|
-
# Get the board with members
|
|
879
|
-
stmt = (
|
|
880
|
-
select(Boards)
|
|
881
|
-
.where(Boards.id == board_id)
|
|
882
|
-
.options(
|
|
883
|
-
selectinload(Boards.owner),
|
|
884
|
-
selectinload(Boards.board_members).selectinload(BoardMembers.user),
|
|
885
|
-
)
|
|
886
|
-
)
|
|
887
|
-
result = await session.execute(stmt)
|
|
888
|
-
board = result.scalar_one_or_none()
|
|
889
|
-
|
|
890
|
-
if not board:
|
|
891
|
-
raise RuntimeError("Board not found")
|
|
892
|
-
|
|
893
|
-
# Cannot remove the board owner
|
|
894
|
-
if board.owner_id == user_id:
|
|
895
|
-
raise RuntimeError("Cannot remove the board owner")
|
|
896
|
-
|
|
897
|
-
# Find the member to remove
|
|
898
|
-
member_to_remove = None
|
|
899
|
-
for member in board.board_members:
|
|
900
|
-
if member.user_id == user_id:
|
|
901
|
-
member_to_remove = member
|
|
902
|
-
break
|
|
903
|
-
|
|
904
|
-
if not member_to_remove:
|
|
905
|
-
raise RuntimeError("User is not a board member")
|
|
906
|
-
|
|
907
|
-
# Check permissions
|
|
908
|
-
is_owner = board.owner_id == auth_context.user_id
|
|
909
|
-
is_admin = any(
|
|
910
|
-
member.user_id == auth_context.user_id and member.role == "admin"
|
|
911
|
-
for member in board.board_members
|
|
912
|
-
)
|
|
913
|
-
is_self = user_id == auth_context.user_id
|
|
914
|
-
|
|
915
|
-
if not is_owner and not is_admin and not is_self:
|
|
916
|
-
raise RuntimeError("Permission denied: insufficient permissions to remove member")
|
|
917
|
-
|
|
918
|
-
# Remove the member
|
|
919
|
-
await session.delete(member_to_remove)
|
|
920
|
-
await session.commit()
|
|
921
|
-
|
|
922
|
-
# Re-query the board with updated members
|
|
923
|
-
stmt = (
|
|
924
|
-
select(Boards)
|
|
925
|
-
.where(Boards.id == board_id)
|
|
926
|
-
.options(
|
|
927
|
-
selectinload(Boards.owner),
|
|
928
|
-
selectinload(Boards.board_members).selectinload(BoardMembers.user),
|
|
929
|
-
)
|
|
930
|
-
)
|
|
931
|
-
result = await session.execute(stmt)
|
|
932
|
-
board = result.scalar_one()
|
|
933
|
-
|
|
934
|
-
logger.info(
|
|
935
|
-
"Board member removed",
|
|
936
|
-
board_id=str(board_id),
|
|
937
|
-
removed_user_id=str(user_id),
|
|
938
|
-
removed_by=str(auth_context.user_id),
|
|
939
|
-
)
|
|
940
|
-
|
|
941
|
-
from ..types.board import Board as BoardType
|
|
942
|
-
|
|
943
|
-
return BoardType(
|
|
944
|
-
id=board.id,
|
|
945
|
-
tenant_id=board.tenant_id,
|
|
946
|
-
owner_id=board.owner_id,
|
|
947
|
-
title=board.title,
|
|
948
|
-
description=board.description,
|
|
949
|
-
is_public=board.is_public,
|
|
950
|
-
settings=board.settings or {},
|
|
951
|
-
metadata=board.metadata_ or {},
|
|
952
|
-
created_at=board.created_at,
|
|
953
|
-
updated_at=board.updated_at,
|
|
954
|
-
)
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
async def update_board_member_role(
|
|
958
|
-
info: strawberry.Info, board_id: UUID, user_id: UUID, role: BoardRole
|
|
959
|
-
) -> Board:
|
|
960
|
-
"""
|
|
961
|
-
Update a board member's role.
|
|
962
|
-
|
|
963
|
-
Only the board owner or an admin member can change member roles.
|
|
964
|
-
The board owner's role cannot be changed (they are always the owner).
|
|
965
|
-
"""
|
|
966
|
-
auth_context = await get_auth_context_from_info(info)
|
|
967
|
-
if not auth_context or not auth_context.is_authenticated:
|
|
968
|
-
raise RuntimeError("Authentication required to update member roles")
|
|
969
|
-
|
|
970
|
-
async with get_async_session() as session:
|
|
971
|
-
# Get the board with members
|
|
972
|
-
stmt = (
|
|
973
|
-
select(Boards)
|
|
974
|
-
.where(Boards.id == board_id)
|
|
975
|
-
.options(
|
|
976
|
-
selectinload(Boards.owner),
|
|
977
|
-
selectinload(Boards.board_members).selectinload(BoardMembers.user),
|
|
978
|
-
)
|
|
979
|
-
)
|
|
980
|
-
result = await session.execute(stmt)
|
|
981
|
-
board = result.scalar_one_or_none()
|
|
982
|
-
|
|
983
|
-
if not board:
|
|
984
|
-
raise RuntimeError("Board not found")
|
|
985
|
-
|
|
986
|
-
# Cannot change the owner's role
|
|
987
|
-
if board.owner_id == user_id:
|
|
988
|
-
raise RuntimeError("Cannot change the board owner's role")
|
|
989
|
-
|
|
990
|
-
# Check permissions: must be owner or admin
|
|
991
|
-
is_owner = board.owner_id == auth_context.user_id
|
|
992
|
-
is_admin = any(
|
|
993
|
-
member.user_id == auth_context.user_id and member.role == "admin"
|
|
994
|
-
for member in board.board_members
|
|
995
|
-
)
|
|
996
|
-
|
|
997
|
-
if not is_owner and not is_admin:
|
|
998
|
-
raise RuntimeError(
|
|
999
|
-
"Permission denied: only board owner or admin can change member roles"
|
|
1000
|
-
)
|
|
1001
|
-
|
|
1002
|
-
# Find the member to update
|
|
1003
|
-
member_to_update = None
|
|
1004
|
-
for member in board.board_members:
|
|
1005
|
-
if member.user_id == user_id:
|
|
1006
|
-
member_to_update = member
|
|
1007
|
-
break
|
|
1008
|
-
|
|
1009
|
-
if not member_to_update:
|
|
1010
|
-
raise RuntimeError("User is not a board member")
|
|
1011
|
-
|
|
1012
|
-
# Update the role
|
|
1013
|
-
old_role = member_to_update.role
|
|
1014
|
-
member_to_update.role = role.value
|
|
1015
|
-
|
|
1016
|
-
await session.commit()
|
|
1017
|
-
await session.refresh(board)
|
|
1018
|
-
|
|
1019
|
-
# Re-query with all relationships loaded
|
|
1020
|
-
stmt = (
|
|
1021
|
-
select(Boards)
|
|
1022
|
-
.where(Boards.id == board_id)
|
|
1023
|
-
.options(
|
|
1024
|
-
selectinload(Boards.owner),
|
|
1025
|
-
selectinload(Boards.board_members).selectinload(BoardMembers.user),
|
|
1026
|
-
)
|
|
1027
|
-
)
|
|
1028
|
-
result = await session.execute(stmt)
|
|
1029
|
-
board = result.scalar_one()
|
|
1030
|
-
|
|
1031
|
-
logger.info(
|
|
1032
|
-
"Board member role updated",
|
|
1033
|
-
board_id=str(board_id),
|
|
1034
|
-
user_id=str(user_id),
|
|
1035
|
-
old_role=old_role,
|
|
1036
|
-
new_role=role.value,
|
|
1037
|
-
updated_by=str(auth_context.user_id),
|
|
1038
|
-
)
|
|
1039
|
-
|
|
1040
|
-
from ..types.board import Board as BoardType
|
|
1041
|
-
|
|
1042
|
-
return BoardType(
|
|
1043
|
-
id=board.id,
|
|
1044
|
-
tenant_id=board.tenant_id,
|
|
1045
|
-
owner_id=board.owner_id,
|
|
1046
|
-
title=board.title,
|
|
1047
|
-
description=board.description,
|
|
1048
|
-
is_public=board.is_public,
|
|
1049
|
-
settings=board.settings or {},
|
|
1050
|
-
metadata=board.metadata_ or {},
|
|
1051
|
-
created_at=board.created_at,
|
|
1052
|
-
updated_at=board.updated_at,
|
|
1053
|
-
)
|