@weirdfingers/baseboards 0.9.5 → 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 +561 -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,666 +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 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 ...generators.registry import registry as generator_registry
|
|
13
|
-
from ...jobs import repository as jobs_repo
|
|
14
|
-
from ...logging import get_logger
|
|
15
|
-
from ...workers.actors import process_generation
|
|
16
|
-
from ..access_control import can_access_board, get_auth_context_from_info
|
|
17
|
-
|
|
18
|
-
if TYPE_CHECKING:
|
|
19
|
-
from ..mutations.root import CreateGenerationInput
|
|
20
|
-
from ..types.board import Board
|
|
21
|
-
from ..types.generation import ArtifactType, Generation, GenerationStatus
|
|
22
|
-
from ..types.user import User
|
|
23
|
-
|
|
24
|
-
logger = get_logger(__name__)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
# Query resolvers
|
|
28
|
-
async def resolve_generation_by_id(info: strawberry.Info, id: UUID) -> Generation | None:
|
|
29
|
-
"""
|
|
30
|
-
Resolve a generation by its ID.
|
|
31
|
-
|
|
32
|
-
Checks authorization: user must have access to the generation's board.
|
|
33
|
-
"""
|
|
34
|
-
auth_context = await get_auth_context_from_info(info)
|
|
35
|
-
if auth_context is None:
|
|
36
|
-
return None
|
|
37
|
-
|
|
38
|
-
async with get_async_session() as session:
|
|
39
|
-
# Query generation
|
|
40
|
-
stmt = select(Generations).where(Generations.id == id)
|
|
41
|
-
result = await session.execute(stmt)
|
|
42
|
-
gen = result.scalar_one_or_none()
|
|
43
|
-
|
|
44
|
-
if not gen:
|
|
45
|
-
logger.info("Generation not found", generation_id=str(id))
|
|
46
|
-
return None
|
|
47
|
-
|
|
48
|
-
# Check board access
|
|
49
|
-
board_stmt = (
|
|
50
|
-
select(Boards)
|
|
51
|
-
.where(Boards.id == gen.board_id)
|
|
52
|
-
.options(selectinload(Boards.board_members))
|
|
53
|
-
)
|
|
54
|
-
board_result = await session.execute(board_stmt)
|
|
55
|
-
board = board_result.scalar_one_or_none()
|
|
56
|
-
|
|
57
|
-
if not board or not can_access_board(board, auth_context):
|
|
58
|
-
logger.info(
|
|
59
|
-
"Access denied to generation",
|
|
60
|
-
generation_id=str(id),
|
|
61
|
-
board_id=str(gen.board_id),
|
|
62
|
-
user_id=(
|
|
63
|
-
str(auth_context.user_id) if auth_context and auth_context.user_id else None
|
|
64
|
-
),
|
|
65
|
-
)
|
|
66
|
-
return None
|
|
67
|
-
|
|
68
|
-
# Convert to GraphQL type
|
|
69
|
-
from ..types.generation import ArtifactType, GenerationStatus
|
|
70
|
-
from ..types.generation import Generation as GenerationType
|
|
71
|
-
|
|
72
|
-
return GenerationType(
|
|
73
|
-
id=gen.id,
|
|
74
|
-
tenant_id=gen.tenant_id,
|
|
75
|
-
board_id=gen.board_id,
|
|
76
|
-
user_id=gen.user_id,
|
|
77
|
-
generator_name=gen.generator_name,
|
|
78
|
-
artifact_type=ArtifactType(gen.artifact_type),
|
|
79
|
-
storage_url=gen.storage_url,
|
|
80
|
-
thumbnail_url=gen.thumbnail_url,
|
|
81
|
-
additional_files=gen.additional_files or [],
|
|
82
|
-
input_params=gen.input_params or {},
|
|
83
|
-
output_metadata=gen.output_metadata or {},
|
|
84
|
-
external_job_id=gen.external_job_id,
|
|
85
|
-
status=GenerationStatus(gen.status),
|
|
86
|
-
progress=float(gen.progress or 0.0),
|
|
87
|
-
error_message=gen.error_message,
|
|
88
|
-
started_at=gen.started_at,
|
|
89
|
-
completed_at=gen.completed_at,
|
|
90
|
-
created_at=gen.created_at,
|
|
91
|
-
updated_at=gen.updated_at,
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
async def resolve_recent_generations(
|
|
96
|
-
info: strawberry.Info,
|
|
97
|
-
board_id: UUID | None,
|
|
98
|
-
status: GenerationStatus | None,
|
|
99
|
-
artifact_type: ArtifactType | None,
|
|
100
|
-
limit: int,
|
|
101
|
-
offset: int,
|
|
102
|
-
) -> list[Generation]:
|
|
103
|
-
"""
|
|
104
|
-
Resolve recent generations with filtering.
|
|
105
|
-
|
|
106
|
-
If board_id is None, returns generations from all boards the user has access to.
|
|
107
|
-
"""
|
|
108
|
-
auth_context = await get_auth_context_from_info(info)
|
|
109
|
-
if not auth_context or not auth_context.is_authenticated:
|
|
110
|
-
logger.info("Unauthenticated access to recent_generations")
|
|
111
|
-
return []
|
|
112
|
-
|
|
113
|
-
async with get_async_session() as session:
|
|
114
|
-
# Build base query
|
|
115
|
-
generations_query = select(Generations)
|
|
116
|
-
|
|
117
|
-
# Apply filters
|
|
118
|
-
if board_id is not None:
|
|
119
|
-
# Check access to specific board
|
|
120
|
-
board_stmt = (
|
|
121
|
-
select(Boards)
|
|
122
|
-
.where(Boards.id == board_id)
|
|
123
|
-
.options(selectinload(Boards.board_members))
|
|
124
|
-
)
|
|
125
|
-
board_result = await session.execute(board_stmt)
|
|
126
|
-
board = board_result.scalar_one_or_none()
|
|
127
|
-
|
|
128
|
-
if not board or not can_access_board(board, auth_context):
|
|
129
|
-
logger.info(
|
|
130
|
-
"Access denied to board for recent generations",
|
|
131
|
-
board_id=str(board_id),
|
|
132
|
-
user_id=str(auth_context.user_id),
|
|
133
|
-
)
|
|
134
|
-
return []
|
|
135
|
-
|
|
136
|
-
generations_query = generations_query.where(Generations.board_id == board_id)
|
|
137
|
-
else:
|
|
138
|
-
# Get all boards user has access to
|
|
139
|
-
member_board_ids = select(BoardMembers.board_id).where(
|
|
140
|
-
BoardMembers.user_id == auth_context.user_id
|
|
141
|
-
)
|
|
142
|
-
accessible_boards_condition = or_(
|
|
143
|
-
Boards.owner_id == auth_context.user_id,
|
|
144
|
-
Boards.id.in_(member_board_ids),
|
|
145
|
-
Boards.is_public,
|
|
146
|
-
)
|
|
147
|
-
accessible_boards_stmt = select(Boards.id).where(accessible_boards_condition)
|
|
148
|
-
accessible_boards_result = await session.execute(accessible_boards_stmt)
|
|
149
|
-
accessible_board_ids = [row[0] for row in accessible_boards_result.all()]
|
|
150
|
-
|
|
151
|
-
if not accessible_board_ids:
|
|
152
|
-
return []
|
|
153
|
-
|
|
154
|
-
generations_query = generations_query.where(
|
|
155
|
-
Generations.board_id.in_(accessible_board_ids)
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
# Apply status filter
|
|
159
|
-
if status is not None:
|
|
160
|
-
generations_query = generations_query.where(Generations.status == status.value)
|
|
161
|
-
|
|
162
|
-
# Apply artifact_type filter
|
|
163
|
-
if artifact_type is not None:
|
|
164
|
-
generations_query = generations_query.where(
|
|
165
|
-
Generations.artifact_type == artifact_type.value
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
# Order by created_at DESC and apply pagination
|
|
169
|
-
generations_query = (
|
|
170
|
-
generations_query.order_by(Generations.created_at.desc()).limit(limit).offset(offset)
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
result = await session.execute(generations_query)
|
|
174
|
-
generations = result.scalars().all()
|
|
175
|
-
|
|
176
|
-
# Convert to GraphQL types
|
|
177
|
-
from ..types.generation import ArtifactType as ArtifactTypeEnum
|
|
178
|
-
from ..types.generation import Generation as GenerationType
|
|
179
|
-
from ..types.generation import GenerationStatus as GenerationStatusEnum
|
|
180
|
-
|
|
181
|
-
return [
|
|
182
|
-
GenerationType(
|
|
183
|
-
id=gen.id,
|
|
184
|
-
tenant_id=gen.tenant_id,
|
|
185
|
-
board_id=gen.board_id,
|
|
186
|
-
user_id=gen.user_id,
|
|
187
|
-
generator_name=gen.generator_name,
|
|
188
|
-
artifact_type=ArtifactTypeEnum(gen.artifact_type),
|
|
189
|
-
storage_url=gen.storage_url,
|
|
190
|
-
thumbnail_url=gen.thumbnail_url,
|
|
191
|
-
additional_files=gen.additional_files or [],
|
|
192
|
-
input_params=gen.input_params or {},
|
|
193
|
-
output_metadata=gen.output_metadata or {},
|
|
194
|
-
external_job_id=gen.external_job_id,
|
|
195
|
-
status=GenerationStatusEnum(gen.status),
|
|
196
|
-
progress=float(gen.progress or 0.0),
|
|
197
|
-
error_message=gen.error_message,
|
|
198
|
-
started_at=gen.started_at,
|
|
199
|
-
completed_at=gen.completed_at,
|
|
200
|
-
created_at=gen.created_at,
|
|
201
|
-
updated_at=gen.updated_at,
|
|
202
|
-
)
|
|
203
|
-
for gen in generations
|
|
204
|
-
]
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
# Field resolvers
|
|
208
|
-
async def resolve_generation_board(generation: Generation, info: strawberry.Info) -> Board:
|
|
209
|
-
"""Resolve the board this generation belongs to."""
|
|
210
|
-
auth_context = await get_auth_context_from_info(info)
|
|
211
|
-
|
|
212
|
-
async with get_async_session() as session:
|
|
213
|
-
stmt = (
|
|
214
|
-
select(Boards)
|
|
215
|
-
.where(Boards.id == generation.board_id)
|
|
216
|
-
.options(
|
|
217
|
-
selectinload(Boards.owner),
|
|
218
|
-
selectinload(Boards.board_members).selectinload(BoardMembers.user),
|
|
219
|
-
)
|
|
220
|
-
)
|
|
221
|
-
result = await session.execute(stmt)
|
|
222
|
-
board = result.scalar_one_or_none()
|
|
223
|
-
|
|
224
|
-
if not board:
|
|
225
|
-
raise RuntimeError("Generation board not found")
|
|
226
|
-
|
|
227
|
-
if not can_access_board(board, auth_context):
|
|
228
|
-
raise RuntimeError("Access denied to generation board")
|
|
229
|
-
|
|
230
|
-
from ..types.board import Board as BoardType
|
|
231
|
-
|
|
232
|
-
return BoardType(
|
|
233
|
-
id=board.id,
|
|
234
|
-
tenant_id=board.tenant_id,
|
|
235
|
-
owner_id=board.owner_id,
|
|
236
|
-
title=board.title,
|
|
237
|
-
description=board.description,
|
|
238
|
-
is_public=board.is_public,
|
|
239
|
-
settings=board.settings or {},
|
|
240
|
-
metadata=board.metadata_ or {},
|
|
241
|
-
created_at=board.created_at,
|
|
242
|
-
updated_at=board.updated_at,
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
async def resolve_generation_user(generation: Generation, info: strawberry.Info) -> User:
|
|
247
|
-
"""Resolve the user who created this generation."""
|
|
248
|
-
async with get_async_session() as session:
|
|
249
|
-
stmt = select(Users).where(Users.id == generation.user_id)
|
|
250
|
-
result = await session.execute(stmt)
|
|
251
|
-
user = result.scalar_one_or_none()
|
|
252
|
-
|
|
253
|
-
if not user:
|
|
254
|
-
raise RuntimeError("Generation user not found")
|
|
255
|
-
|
|
256
|
-
from ..types.user import User as UserType
|
|
257
|
-
|
|
258
|
-
return UserType(
|
|
259
|
-
id=user.id,
|
|
260
|
-
tenant_id=user.tenant_id,
|
|
261
|
-
auth_provider=user.auth_provider,
|
|
262
|
-
auth_subject=user.auth_subject,
|
|
263
|
-
email=user.email,
|
|
264
|
-
display_name=user.display_name,
|
|
265
|
-
avatar_url=user.avatar_url,
|
|
266
|
-
created_at=user.created_at,
|
|
267
|
-
updated_at=user.updated_at,
|
|
268
|
-
)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
async def resolve_generation_parent(
|
|
272
|
-
generation: Generation, info: strawberry.Info
|
|
273
|
-
) -> Generation | None:
|
|
274
|
-
"""DEPRECATED: Use ancestry field instead. This resolver is no longer used."""
|
|
275
|
-
return None
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
async def resolve_generation_inputs(
|
|
279
|
-
generation: Generation, info: strawberry.Info
|
|
280
|
-
) -> list[Generation]: # noqa: E501
|
|
281
|
-
"""DEPRECATED: Use input_artifacts field instead. This resolver is no longer used."""
|
|
282
|
-
return []
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
async def resolve_generation_children(
|
|
286
|
-
generation: Generation, info: strawberry.Info
|
|
287
|
-
) -> list[Generation]: # noqa: E501
|
|
288
|
-
"""DEPRECATED: Use descendants field instead. This resolver is no longer used."""
|
|
289
|
-
return []
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
# Mutation resolvers
|
|
293
|
-
async def create_generation(info: strawberry.Info, input: CreateGenerationInput) -> Generation:
|
|
294
|
-
"""
|
|
295
|
-
Create a new generation and enqueue it for processing.
|
|
296
|
-
|
|
297
|
-
Requires editor or owner role on the target board.
|
|
298
|
-
"""
|
|
299
|
-
auth_context = await get_auth_context_from_info(info)
|
|
300
|
-
if not auth_context or not auth_context.is_authenticated or not auth_context.user_id:
|
|
301
|
-
raise RuntimeError("Authentication required to create a generation")
|
|
302
|
-
|
|
303
|
-
async with get_async_session() as session:
|
|
304
|
-
# Check board access - require editor or owner role
|
|
305
|
-
board_stmt = (
|
|
306
|
-
select(Boards)
|
|
307
|
-
.where(Boards.id == input.board_id)
|
|
308
|
-
.options(selectinload(Boards.board_members))
|
|
309
|
-
)
|
|
310
|
-
board_result = await session.execute(board_stmt)
|
|
311
|
-
board = board_result.scalar_one_or_none()
|
|
312
|
-
|
|
313
|
-
if not board:
|
|
314
|
-
raise RuntimeError("Board not found")
|
|
315
|
-
|
|
316
|
-
# Check if user is owner or editor
|
|
317
|
-
is_owner = board.owner_id == auth_context.user_id
|
|
318
|
-
is_editor = any(
|
|
319
|
-
member.user_id == auth_context.user_id and member.role in {"editor", "admin"}
|
|
320
|
-
for member in board.board_members
|
|
321
|
-
)
|
|
322
|
-
|
|
323
|
-
if not is_owner and not is_editor:
|
|
324
|
-
raise RuntimeError(
|
|
325
|
-
"Permission denied: only board owner or editor can create generations"
|
|
326
|
-
)
|
|
327
|
-
|
|
328
|
-
# Validate generator exists
|
|
329
|
-
generator = generator_registry.get(input.generator_name)
|
|
330
|
-
if generator is None:
|
|
331
|
-
raise RuntimeError(f"Unknown generator: {input.generator_name}")
|
|
332
|
-
|
|
333
|
-
# Create generation record
|
|
334
|
-
# Note: Lineage will be captured automatically during generation processing
|
|
335
|
-
# via artifact resolution in the worker (see actors.py)
|
|
336
|
-
gen = await jobs_repo.create_generation(
|
|
337
|
-
session,
|
|
338
|
-
tenant_id=auth_context.tenant_id,
|
|
339
|
-
board_id=input.board_id,
|
|
340
|
-
user_id=auth_context.user_id,
|
|
341
|
-
generator_name=input.generator_name,
|
|
342
|
-
artifact_type=input.artifact_type.value,
|
|
343
|
-
input_params=input.input_params,
|
|
344
|
-
)
|
|
345
|
-
|
|
346
|
-
await session.commit()
|
|
347
|
-
await session.refresh(gen)
|
|
348
|
-
|
|
349
|
-
logger.info(
|
|
350
|
-
"Generation created",
|
|
351
|
-
generation_id=str(gen.id),
|
|
352
|
-
board_id=str(input.board_id),
|
|
353
|
-
user_id=str(auth_context.user_id),
|
|
354
|
-
generator_name=input.generator_name,
|
|
355
|
-
)
|
|
356
|
-
|
|
357
|
-
# Enqueue job for processing
|
|
358
|
-
message = process_generation.send(str(gen.id))
|
|
359
|
-
logger.info(
|
|
360
|
-
"Generation job enqueued",
|
|
361
|
-
generation_id=str(gen.id),
|
|
362
|
-
message_id=message.message_id,
|
|
363
|
-
queue_name=message.queue_name,
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
# Convert to GraphQL type
|
|
367
|
-
from ..types.generation import ArtifactType, GenerationStatus
|
|
368
|
-
from ..types.generation import Generation as GenerationType
|
|
369
|
-
|
|
370
|
-
return GenerationType(
|
|
371
|
-
id=gen.id,
|
|
372
|
-
tenant_id=gen.tenant_id,
|
|
373
|
-
board_id=gen.board_id,
|
|
374
|
-
user_id=gen.user_id,
|
|
375
|
-
generator_name=gen.generator_name,
|
|
376
|
-
artifact_type=ArtifactType(gen.artifact_type),
|
|
377
|
-
storage_url=gen.storage_url,
|
|
378
|
-
thumbnail_url=gen.thumbnail_url,
|
|
379
|
-
additional_files=gen.additional_files or [],
|
|
380
|
-
input_params=gen.input_params or {},
|
|
381
|
-
output_metadata=gen.output_metadata or {},
|
|
382
|
-
external_job_id=gen.external_job_id,
|
|
383
|
-
status=GenerationStatus(gen.status),
|
|
384
|
-
progress=float(gen.progress or 0.0),
|
|
385
|
-
error_message=gen.error_message,
|
|
386
|
-
started_at=gen.started_at,
|
|
387
|
-
completed_at=gen.completed_at,
|
|
388
|
-
created_at=gen.created_at,
|
|
389
|
-
updated_at=gen.updated_at,
|
|
390
|
-
)
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
async def cancel_generation(info: strawberry.Info, id: UUID) -> Generation:
|
|
394
|
-
"""
|
|
395
|
-
Cancel a pending or processing generation.
|
|
396
|
-
|
|
397
|
-
Requires ownership or editor role on the board.
|
|
398
|
-
"""
|
|
399
|
-
auth_context = await get_auth_context_from_info(info)
|
|
400
|
-
if not auth_context or not auth_context.is_authenticated:
|
|
401
|
-
raise RuntimeError("Authentication required to cancel a generation")
|
|
402
|
-
|
|
403
|
-
async with get_async_session() as session:
|
|
404
|
-
# Query generation
|
|
405
|
-
stmt = select(Generations).where(Generations.id == id)
|
|
406
|
-
result = await session.execute(stmt)
|
|
407
|
-
gen = result.scalar_one_or_none()
|
|
408
|
-
|
|
409
|
-
if not gen:
|
|
410
|
-
raise RuntimeError("Generation not found")
|
|
411
|
-
|
|
412
|
-
# Check board access and permissions
|
|
413
|
-
board_stmt = (
|
|
414
|
-
select(Boards)
|
|
415
|
-
.where(Boards.id == gen.board_id)
|
|
416
|
-
.options(selectinload(Boards.board_members))
|
|
417
|
-
)
|
|
418
|
-
board_result = await session.execute(board_stmt)
|
|
419
|
-
board = board_result.scalar_one_or_none()
|
|
420
|
-
|
|
421
|
-
if not board:
|
|
422
|
-
raise RuntimeError("Board not found")
|
|
423
|
-
|
|
424
|
-
# Check authorization
|
|
425
|
-
is_owner = board.owner_id == auth_context.user_id
|
|
426
|
-
is_editor = any(
|
|
427
|
-
member.user_id == auth_context.user_id and member.role in {"editor", "admin"}
|
|
428
|
-
for member in board.board_members
|
|
429
|
-
)
|
|
430
|
-
is_creator = gen.user_id == auth_context.user_id
|
|
431
|
-
|
|
432
|
-
# Owner can cancel any generation, editor can only cancel their own
|
|
433
|
-
if not is_owner and not (is_editor and is_creator):
|
|
434
|
-
raise RuntimeError(
|
|
435
|
-
"Permission denied: only board owner or generation creator can cancel"
|
|
436
|
-
)
|
|
437
|
-
|
|
438
|
-
# Validate status
|
|
439
|
-
if gen.status not in {"pending", "processing"}:
|
|
440
|
-
raise RuntimeError(
|
|
441
|
-
f"Cannot cancel generation with status '{gen.status}'. "
|
|
442
|
-
"Only pending or processing generations can be cancelled."
|
|
443
|
-
)
|
|
444
|
-
|
|
445
|
-
# Update status to cancelled
|
|
446
|
-
await jobs_repo.update_progress(
|
|
447
|
-
session,
|
|
448
|
-
id,
|
|
449
|
-
status="cancelled",
|
|
450
|
-
progress=float(gen.progress or 0.0),
|
|
451
|
-
error_message="Cancelled by user",
|
|
452
|
-
)
|
|
453
|
-
await session.commit()
|
|
454
|
-
|
|
455
|
-
# Refresh to get updated data
|
|
456
|
-
await session.refresh(gen)
|
|
457
|
-
|
|
458
|
-
logger.info(
|
|
459
|
-
"Generation cancelled",
|
|
460
|
-
generation_id=str(id),
|
|
461
|
-
user_id=str(auth_context.user_id),
|
|
462
|
-
)
|
|
463
|
-
|
|
464
|
-
# Convert to GraphQL type
|
|
465
|
-
from ..types.generation import ArtifactType, GenerationStatus
|
|
466
|
-
from ..types.generation import Generation as GenerationType
|
|
467
|
-
|
|
468
|
-
return GenerationType(
|
|
469
|
-
id=gen.id,
|
|
470
|
-
tenant_id=gen.tenant_id,
|
|
471
|
-
board_id=gen.board_id,
|
|
472
|
-
user_id=gen.user_id,
|
|
473
|
-
generator_name=gen.generator_name,
|
|
474
|
-
artifact_type=ArtifactType(gen.artifact_type),
|
|
475
|
-
storage_url=gen.storage_url,
|
|
476
|
-
thumbnail_url=gen.thumbnail_url,
|
|
477
|
-
additional_files=gen.additional_files or [],
|
|
478
|
-
input_params=gen.input_params or {},
|
|
479
|
-
output_metadata=gen.output_metadata or {},
|
|
480
|
-
external_job_id=gen.external_job_id,
|
|
481
|
-
status=GenerationStatus(gen.status),
|
|
482
|
-
progress=float(gen.progress or 0.0),
|
|
483
|
-
error_message=gen.error_message,
|
|
484
|
-
started_at=gen.started_at,
|
|
485
|
-
completed_at=gen.completed_at,
|
|
486
|
-
created_at=gen.created_at,
|
|
487
|
-
updated_at=gen.updated_at,
|
|
488
|
-
)
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
async def delete_generation(info: strawberry.Info, id: UUID) -> bool:
|
|
492
|
-
"""
|
|
493
|
-
Delete a generation and its associated storage artifacts.
|
|
494
|
-
|
|
495
|
-
Requires ownership or editor role on the board.
|
|
496
|
-
"""
|
|
497
|
-
auth_context = await get_auth_context_from_info(info)
|
|
498
|
-
if not auth_context or not auth_context.is_authenticated:
|
|
499
|
-
raise RuntimeError("Authentication required to delete a generation")
|
|
500
|
-
|
|
501
|
-
async with get_async_session() as session:
|
|
502
|
-
# Query generation
|
|
503
|
-
stmt = select(Generations).where(Generations.id == id)
|
|
504
|
-
result = await session.execute(stmt)
|
|
505
|
-
gen = result.scalar_one_or_none()
|
|
506
|
-
|
|
507
|
-
if not gen:
|
|
508
|
-
raise RuntimeError("Generation not found")
|
|
509
|
-
|
|
510
|
-
# Check board access and permissions
|
|
511
|
-
board_stmt = (
|
|
512
|
-
select(Boards)
|
|
513
|
-
.where(Boards.id == gen.board_id)
|
|
514
|
-
.options(selectinload(Boards.board_members))
|
|
515
|
-
)
|
|
516
|
-
board_result = await session.execute(board_stmt)
|
|
517
|
-
board = board_result.scalar_one_or_none()
|
|
518
|
-
|
|
519
|
-
if not board:
|
|
520
|
-
raise RuntimeError("Board not found")
|
|
521
|
-
|
|
522
|
-
# Check authorization
|
|
523
|
-
is_owner = board.owner_id == auth_context.user_id
|
|
524
|
-
is_editor = any(
|
|
525
|
-
member.user_id == auth_context.user_id and member.role in {"editor", "admin"}
|
|
526
|
-
for member in board.board_members
|
|
527
|
-
)
|
|
528
|
-
is_creator = gen.user_id == auth_context.user_id
|
|
529
|
-
|
|
530
|
-
# Owner can delete any generation, editor can only delete their own
|
|
531
|
-
if not is_owner and not (is_editor and is_creator):
|
|
532
|
-
raise RuntimeError(
|
|
533
|
-
"Permission denied: only board owner or generation creator can delete"
|
|
534
|
-
)
|
|
535
|
-
|
|
536
|
-
# Delete storage artifacts
|
|
537
|
-
# TODO: Full storage deletion requires storage_key and storage_provider fields
|
|
538
|
-
# to be added to the Generations table. For now, we log the deletion intent.
|
|
539
|
-
# Once those fields are added, use: await storage_manager.delete_artifact(key, provider)
|
|
540
|
-
|
|
541
|
-
if gen.storage_url or gen.thumbnail_url or gen.additional_files:
|
|
542
|
-
logger.info(
|
|
543
|
-
"Storage artifact deletion",
|
|
544
|
-
generation_id=str(id),
|
|
545
|
-
has_storage_url=bool(gen.storage_url),
|
|
546
|
-
has_thumbnail=bool(gen.thumbnail_url),
|
|
547
|
-
additional_files_count=(len(gen.additional_files) if gen.additional_files else 0),
|
|
548
|
-
)
|
|
549
|
-
logger.warning(
|
|
550
|
-
"Storage artifact deletion not yet implemented - requires storage_key and "
|
|
551
|
-
"storage_provider fields in Generations table",
|
|
552
|
-
generation_id=str(id),
|
|
553
|
-
)
|
|
554
|
-
|
|
555
|
-
# Delete generation from database
|
|
556
|
-
await session.delete(gen)
|
|
557
|
-
await session.commit()
|
|
558
|
-
|
|
559
|
-
logger.info(
|
|
560
|
-
"Generation deleted",
|
|
561
|
-
generation_id=str(id),
|
|
562
|
-
user_id=str(auth_context.user_id),
|
|
563
|
-
)
|
|
564
|
-
|
|
565
|
-
return True
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
async def regenerate(info: strawberry.Info, id: UUID) -> Generation:
|
|
569
|
-
"""
|
|
570
|
-
Regenerate from an existing generation.
|
|
571
|
-
|
|
572
|
-
Creates a new generation with the same inputs and sets the original as parent.
|
|
573
|
-
"""
|
|
574
|
-
auth_context = await get_auth_context_from_info(info)
|
|
575
|
-
if not auth_context or not auth_context.is_authenticated or not auth_context.user_id:
|
|
576
|
-
raise RuntimeError("Authentication required to regenerate")
|
|
577
|
-
|
|
578
|
-
async with get_async_session() as session:
|
|
579
|
-
# Query original generation
|
|
580
|
-
stmt = select(Generations).where(Generations.id == id)
|
|
581
|
-
result = await session.execute(stmt)
|
|
582
|
-
original = result.scalar_one_or_none()
|
|
583
|
-
|
|
584
|
-
if not original:
|
|
585
|
-
raise RuntimeError("Original generation not found")
|
|
586
|
-
|
|
587
|
-
# Check board access - require editor or owner role
|
|
588
|
-
board_stmt = (
|
|
589
|
-
select(Boards)
|
|
590
|
-
.where(Boards.id == original.board_id)
|
|
591
|
-
.options(selectinload(Boards.board_members))
|
|
592
|
-
)
|
|
593
|
-
board_result = await session.execute(board_stmt)
|
|
594
|
-
board = board_result.scalar_one_or_none()
|
|
595
|
-
|
|
596
|
-
if not board:
|
|
597
|
-
raise RuntimeError("Board not found")
|
|
598
|
-
|
|
599
|
-
# Check if user is owner or editor
|
|
600
|
-
is_owner = board.owner_id == auth_context.user_id
|
|
601
|
-
is_editor = any(
|
|
602
|
-
member.user_id == auth_context.user_id and member.role in {"editor", "admin"}
|
|
603
|
-
for member in board.board_members
|
|
604
|
-
)
|
|
605
|
-
|
|
606
|
-
if not is_owner and not is_editor:
|
|
607
|
-
raise RuntimeError("Permission denied: only board owner or editor can regenerate")
|
|
608
|
-
|
|
609
|
-
# Validate generator still exists
|
|
610
|
-
generator = generator_registry.get(original.generator_name)
|
|
611
|
-
if generator is None:
|
|
612
|
-
raise RuntimeError(f"Generator '{original.generator_name}' is no longer available")
|
|
613
|
-
|
|
614
|
-
# Create new generation with copied inputs
|
|
615
|
-
new_gen = await jobs_repo.create_generation(
|
|
616
|
-
session,
|
|
617
|
-
tenant_id=original.tenant_id,
|
|
618
|
-
board_id=original.board_id,
|
|
619
|
-
user_id=auth_context.user_id,
|
|
620
|
-
generator_name=original.generator_name,
|
|
621
|
-
artifact_type=original.artifact_type,
|
|
622
|
-
input_params=original.input_params or {},
|
|
623
|
-
)
|
|
624
|
-
|
|
625
|
-
# Note: Lineage will be captured automatically during generation processing
|
|
626
|
-
# via artifact resolution in the worker (see actors.py)
|
|
627
|
-
|
|
628
|
-
await session.commit()
|
|
629
|
-
await session.refresh(new_gen)
|
|
630
|
-
|
|
631
|
-
logger.info(
|
|
632
|
-
"Generation regenerated",
|
|
633
|
-
new_generation_id=str(new_gen.id),
|
|
634
|
-
original_generation_id=str(id),
|
|
635
|
-
user_id=str(auth_context.user_id),
|
|
636
|
-
)
|
|
637
|
-
|
|
638
|
-
# Enqueue job for processing
|
|
639
|
-
process_generation.send(str(new_gen.id))
|
|
640
|
-
logger.info("Regeneration job enqueued", generation_id=str(new_gen.id))
|
|
641
|
-
|
|
642
|
-
# Convert to GraphQL type
|
|
643
|
-
from ..types.generation import ArtifactType, GenerationStatus
|
|
644
|
-
from ..types.generation import Generation as GenerationType
|
|
645
|
-
|
|
646
|
-
return GenerationType(
|
|
647
|
-
id=new_gen.id,
|
|
648
|
-
tenant_id=new_gen.tenant_id,
|
|
649
|
-
board_id=new_gen.board_id,
|
|
650
|
-
user_id=new_gen.user_id,
|
|
651
|
-
generator_name=new_gen.generator_name,
|
|
652
|
-
artifact_type=ArtifactType(new_gen.artifact_type),
|
|
653
|
-
storage_url=new_gen.storage_url,
|
|
654
|
-
thumbnail_url=new_gen.thumbnail_url,
|
|
655
|
-
additional_files=new_gen.additional_files or [],
|
|
656
|
-
input_params=new_gen.input_params or {},
|
|
657
|
-
output_metadata=new_gen.output_metadata or {},
|
|
658
|
-
external_job_id=new_gen.external_job_id,
|
|
659
|
-
status=GenerationStatus(new_gen.status),
|
|
660
|
-
progress=float(new_gen.progress or 0.0),
|
|
661
|
-
error_message=new_gen.error_message,
|
|
662
|
-
started_at=new_gen.started_at,
|
|
663
|
-
completed_at=new_gen.completed_at,
|
|
664
|
-
created_at=new_gen.created_at,
|
|
665
|
-
updated_at=new_gen.updated_at,
|
|
666
|
-
)
|