@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,463 +0,0 @@
|
|
|
1
|
-
"""Resolvers for artifact upload operations."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import ipaddress
|
|
6
|
-
from datetime import UTC, datetime
|
|
7
|
-
from decimal import Decimal
|
|
8
|
-
from typing import TYPE_CHECKING
|
|
9
|
-
from urllib.parse import urlparse
|
|
10
|
-
from uuid import UUID
|
|
11
|
-
|
|
12
|
-
import aiohttp
|
|
13
|
-
import strawberry
|
|
14
|
-
from sqlalchemy import select
|
|
15
|
-
from sqlalchemy.orm import selectinload
|
|
16
|
-
|
|
17
|
-
from ...auth.context import AuthContext
|
|
18
|
-
from ...database.connection import get_async_session
|
|
19
|
-
from ...dbmodels import Boards, Generations
|
|
20
|
-
from ...logging import get_logger
|
|
21
|
-
from ...storage.factory import create_storage_manager
|
|
22
|
-
from ..access_control import get_auth_context_from_info
|
|
23
|
-
from ..types.generation import ArtifactType
|
|
24
|
-
|
|
25
|
-
if TYPE_CHECKING:
|
|
26
|
-
from ..types.generation import Generation as GenerationType
|
|
27
|
-
from ..types.generation import UploadArtifactInput
|
|
28
|
-
|
|
29
|
-
logger = get_logger(__name__)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def _validate_mime_type(
|
|
33
|
-
content_type: str, artifact_type: ArtifactType, filename: str | None
|
|
34
|
-
) -> tuple[bool, str | None]:
|
|
35
|
-
"""
|
|
36
|
-
Validate that MIME type matches the expected artifact type.
|
|
37
|
-
|
|
38
|
-
Args:
|
|
39
|
-
content_type: The MIME type to validate (e.g., "image/jpeg")
|
|
40
|
-
artifact_type: The expected artifact type enum
|
|
41
|
-
filename: Optional filename for additional context
|
|
42
|
-
|
|
43
|
-
Returns:
|
|
44
|
-
Tuple of (is_valid, error_message)
|
|
45
|
-
"""
|
|
46
|
-
# Define allowed MIME types for each artifact type
|
|
47
|
-
allowed_mime_types = {
|
|
48
|
-
ArtifactType.IMAGE: [
|
|
49
|
-
"image/jpeg",
|
|
50
|
-
"image/jpg",
|
|
51
|
-
"image/png",
|
|
52
|
-
"image/gif",
|
|
53
|
-
"image/webp",
|
|
54
|
-
"image/bmp",
|
|
55
|
-
"image/svg+xml",
|
|
56
|
-
],
|
|
57
|
-
ArtifactType.VIDEO: [
|
|
58
|
-
"video/mp4",
|
|
59
|
-
"video/quicktime",
|
|
60
|
-
"video/x-msvideo",
|
|
61
|
-
"video/webm",
|
|
62
|
-
"video/mpeg",
|
|
63
|
-
"video/x-matroska",
|
|
64
|
-
],
|
|
65
|
-
ArtifactType.AUDIO: [
|
|
66
|
-
"audio/mpeg",
|
|
67
|
-
"audio/mp3",
|
|
68
|
-
"audio/wav",
|
|
69
|
-
"audio/ogg",
|
|
70
|
-
"audio/webm",
|
|
71
|
-
"audio/x-m4a",
|
|
72
|
-
"audio/mp4",
|
|
73
|
-
],
|
|
74
|
-
ArtifactType.TEXT: [
|
|
75
|
-
"text/plain",
|
|
76
|
-
"text/markdown",
|
|
77
|
-
"application/json",
|
|
78
|
-
"text/html",
|
|
79
|
-
"text/csv",
|
|
80
|
-
],
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
# Normalize MIME type (remove charset, etc.)
|
|
84
|
-
mime_type = content_type.split(";")[0].strip().lower()
|
|
85
|
-
|
|
86
|
-
# Check if artifact type is supported
|
|
87
|
-
if artifact_type not in allowed_mime_types:
|
|
88
|
-
return False, f"Unsupported artifact type: {artifact_type.value}"
|
|
89
|
-
|
|
90
|
-
# Check if MIME type is allowed for this artifact type
|
|
91
|
-
if mime_type not in allowed_mime_types[artifact_type]:
|
|
92
|
-
# Also check for generic types
|
|
93
|
-
mime_category = mime_type.split("/")[0]
|
|
94
|
-
if mime_category != artifact_type.value:
|
|
95
|
-
return (
|
|
96
|
-
False,
|
|
97
|
-
f"MIME type '{mime_type}' does not match artifact type '{artifact_type.value}'",
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
return True, None
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def _is_safe_url(url: str) -> tuple[bool, str | None]:
|
|
104
|
-
"""
|
|
105
|
-
Validate URL to prevent SSRF attacks.
|
|
106
|
-
|
|
107
|
-
Returns:
|
|
108
|
-
Tuple of (is_safe, error_message)
|
|
109
|
-
"""
|
|
110
|
-
try:
|
|
111
|
-
parsed = urlparse(url)
|
|
112
|
-
|
|
113
|
-
# Only allow http and https
|
|
114
|
-
if parsed.scheme not in ("http", "https"):
|
|
115
|
-
return (
|
|
116
|
-
False,
|
|
117
|
-
f"URL scheme '{parsed.scheme}' not allowed. Only http and https are supported.",
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
hostname = parsed.hostname
|
|
121
|
-
if not hostname:
|
|
122
|
-
return False, "Invalid URL: no hostname found"
|
|
123
|
-
|
|
124
|
-
# Block localhost
|
|
125
|
-
if hostname.lower() in ("localhost", "127.0.0.1", "::1"):
|
|
126
|
-
return False, "Access to localhost is not allowed"
|
|
127
|
-
|
|
128
|
-
# Try to resolve hostname to IP
|
|
129
|
-
try:
|
|
130
|
-
# Check if it's already an IP address
|
|
131
|
-
ip = ipaddress.ip_address(hostname)
|
|
132
|
-
|
|
133
|
-
# Block private IP ranges
|
|
134
|
-
if ip.is_private:
|
|
135
|
-
return False, f"Access to private IP address {ip} is not allowed"
|
|
136
|
-
|
|
137
|
-
# Block link-local addresses (including AWS metadata endpoint)
|
|
138
|
-
if ip.is_link_local:
|
|
139
|
-
return False, f"Access to link-local address {ip} is not allowed"
|
|
140
|
-
|
|
141
|
-
# Block loopback
|
|
142
|
-
if ip.is_loopback:
|
|
143
|
-
return False, f"Access to loopback address {ip} is not allowed"
|
|
144
|
-
|
|
145
|
-
except ValueError:
|
|
146
|
-
# Not an IP address, it's a hostname - this is OK
|
|
147
|
-
# In production, you might want to resolve the hostname and check the IP
|
|
148
|
-
# but that adds complexity and potential DNS rebinding issues
|
|
149
|
-
pass
|
|
150
|
-
|
|
151
|
-
return True, None
|
|
152
|
-
|
|
153
|
-
except Exception as e:
|
|
154
|
-
return False, f"Invalid URL: {e}"
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
async def upload_artifact_from_url(
|
|
158
|
-
info: strawberry.Info,
|
|
159
|
-
input: UploadArtifactInput,
|
|
160
|
-
) -> GenerationType:
|
|
161
|
-
"""Upload artifact from URL (synchronous)."""
|
|
162
|
-
from ...config import settings
|
|
163
|
-
|
|
164
|
-
auth_context = await get_auth_context_from_info(info)
|
|
165
|
-
if not auth_context or not auth_context.is_authenticated:
|
|
166
|
-
raise RuntimeError("Authentication required")
|
|
167
|
-
|
|
168
|
-
if not input.file_url:
|
|
169
|
-
raise RuntimeError("file_url is required")
|
|
170
|
-
|
|
171
|
-
# Validate URL to prevent SSRF attacks
|
|
172
|
-
is_safe, error_msg = _is_safe_url(input.file_url)
|
|
173
|
-
if not is_safe:
|
|
174
|
-
logger.warning("Unsafe URL blocked", url=input.file_url, reason=error_msg)
|
|
175
|
-
raise RuntimeError(f"URL not allowed: {error_msg}")
|
|
176
|
-
|
|
177
|
-
# Download file from URL
|
|
178
|
-
async with aiohttp.ClientSession() as http_session:
|
|
179
|
-
try:
|
|
180
|
-
async with http_session.get(
|
|
181
|
-
input.file_url, timeout=aiohttp.ClientTimeout(total=60)
|
|
182
|
-
) as resp:
|
|
183
|
-
if resp.status != 200:
|
|
184
|
-
raise RuntimeError(f"Failed to download from URL: HTTP {resp.status}")
|
|
185
|
-
|
|
186
|
-
# Check Content-Length before downloading to prevent memory exhaustion
|
|
187
|
-
content_length = resp.headers.get("Content-Length")
|
|
188
|
-
if content_length:
|
|
189
|
-
file_size = int(content_length)
|
|
190
|
-
if file_size > settings.max_upload_size:
|
|
191
|
-
raise RuntimeError(
|
|
192
|
-
f"File size ({file_size} bytes) exceeds maximum allowed "
|
|
193
|
-
f"size ({settings.max_upload_size} bytes)"
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
content = await resp.read()
|
|
197
|
-
content_type = resp.headers.get("Content-Type", "application/octet-stream")
|
|
198
|
-
|
|
199
|
-
# Extract filename from URL if not provided
|
|
200
|
-
filename = input.original_filename
|
|
201
|
-
if not filename:
|
|
202
|
-
path = urlparse(input.file_url).path
|
|
203
|
-
filename = path.split("/")[-1] if path else "uploaded_file"
|
|
204
|
-
|
|
205
|
-
except aiohttp.ClientError as e:
|
|
206
|
-
logger.error("URL download failed", url=input.file_url, error=str(e))
|
|
207
|
-
raise RuntimeError("Failed to download file from URL") from e
|
|
208
|
-
|
|
209
|
-
# Process upload
|
|
210
|
-
return await _process_upload(
|
|
211
|
-
auth_context=auth_context,
|
|
212
|
-
board_id=input.board_id,
|
|
213
|
-
artifact_type=input.artifact_type,
|
|
214
|
-
file_content=content,
|
|
215
|
-
filename=filename,
|
|
216
|
-
content_type=content_type,
|
|
217
|
-
user_description=input.user_description,
|
|
218
|
-
parent_generation_id=input.parent_generation_id,
|
|
219
|
-
upload_source="url",
|
|
220
|
-
source_url=input.file_url,
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
async def upload_artifact_from_file(
|
|
225
|
-
auth_context: AuthContext,
|
|
226
|
-
board_id: UUID,
|
|
227
|
-
artifact_type: str,
|
|
228
|
-
file_content: bytes,
|
|
229
|
-
filename: str | None,
|
|
230
|
-
content_type: str | None,
|
|
231
|
-
user_description: str | None,
|
|
232
|
-
parent_generation_id: UUID | None,
|
|
233
|
-
) -> GenerationType:
|
|
234
|
-
"""Upload artifact from file (synchronous)."""
|
|
235
|
-
return await _process_upload(
|
|
236
|
-
auth_context=auth_context,
|
|
237
|
-
board_id=board_id,
|
|
238
|
-
artifact_type=ArtifactType(artifact_type),
|
|
239
|
-
file_content=file_content,
|
|
240
|
-
filename=filename or "uploaded_file",
|
|
241
|
-
content_type=content_type or "application/octet-stream",
|
|
242
|
-
user_description=user_description,
|
|
243
|
-
parent_generation_id=parent_generation_id,
|
|
244
|
-
upload_source="file",
|
|
245
|
-
source_url=None,
|
|
246
|
-
)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
def _sanitize_filename(filename: str) -> str:
|
|
250
|
-
"""
|
|
251
|
-
Sanitize filename to prevent path traversal and other security issues.
|
|
252
|
-
|
|
253
|
-
Returns:
|
|
254
|
-
Sanitized filename (basename only, no path components)
|
|
255
|
-
"""
|
|
256
|
-
import os
|
|
257
|
-
import re
|
|
258
|
-
|
|
259
|
-
# Get basename only (remove any path components)
|
|
260
|
-
filename = os.path.basename(filename)
|
|
261
|
-
|
|
262
|
-
# Remove any null bytes
|
|
263
|
-
filename = filename.replace("\x00", "")
|
|
264
|
-
|
|
265
|
-
# Replace potentially dangerous characters (including backslash for Windows paths)
|
|
266
|
-
filename = re.sub(r'[<>:"|?*\\]', "_", filename)
|
|
267
|
-
|
|
268
|
-
# Remove leading/trailing whitespace and dots
|
|
269
|
-
filename = filename.strip(". ")
|
|
270
|
-
|
|
271
|
-
# If filename is empty after sanitization, use a default
|
|
272
|
-
if not filename:
|
|
273
|
-
filename = "uploaded_file"
|
|
274
|
-
|
|
275
|
-
return filename
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
async def _process_upload(
|
|
279
|
-
auth_context: AuthContext,
|
|
280
|
-
board_id: UUID,
|
|
281
|
-
artifact_type: ArtifactType,
|
|
282
|
-
file_content: bytes,
|
|
283
|
-
filename: str,
|
|
284
|
-
content_type: str,
|
|
285
|
-
user_description: str | None,
|
|
286
|
-
parent_generation_id: UUID | None,
|
|
287
|
-
upload_source: str,
|
|
288
|
-
source_url: str | None,
|
|
289
|
-
) -> GenerationType:
|
|
290
|
-
"""Common upload processing logic.
|
|
291
|
-
|
|
292
|
-
Args:
|
|
293
|
-
auth_context: Authentication context for the request
|
|
294
|
-
board_id: UUID of the board to upload to
|
|
295
|
-
artifact_type: Type of artifact being uploaded (enum)
|
|
296
|
-
file_content: Binary content of the file
|
|
297
|
-
filename: Original filename
|
|
298
|
-
content_type: MIME type of the file
|
|
299
|
-
user_description: Optional user-provided description
|
|
300
|
-
parent_generation_id: Optional parent generation UUID
|
|
301
|
-
upload_source: Source of upload ("file" or "url")
|
|
302
|
-
source_url: URL if uploaded from URL, None otherwise
|
|
303
|
-
|
|
304
|
-
Returns:
|
|
305
|
-
GenerationType object representing the uploaded artifact
|
|
306
|
-
"""
|
|
307
|
-
from ...config import settings
|
|
308
|
-
from ..types.generation import Generation as GenerationType
|
|
309
|
-
from ..types.generation import GenerationStatus
|
|
310
|
-
|
|
311
|
-
# Sanitize filename to prevent path traversal
|
|
312
|
-
filename = _sanitize_filename(filename)
|
|
313
|
-
|
|
314
|
-
# Validate MIME type matches artifact type
|
|
315
|
-
is_valid, error_msg = _validate_mime_type(content_type, artifact_type, filename)
|
|
316
|
-
if not is_valid:
|
|
317
|
-
logger.warning(
|
|
318
|
-
"Invalid MIME type for artifact",
|
|
319
|
-
mime_type=content_type,
|
|
320
|
-
artifact_type=artifact_type.value,
|
|
321
|
-
reason=error_msg,
|
|
322
|
-
)
|
|
323
|
-
raise RuntimeError(f"Invalid file type: {error_msg}")
|
|
324
|
-
|
|
325
|
-
# Validate file size (double-check even after Content-Length check)
|
|
326
|
-
if len(file_content) > settings.max_upload_size:
|
|
327
|
-
raise RuntimeError(
|
|
328
|
-
f"File size ({len(file_content)} bytes) exceeds maximum allowed "
|
|
329
|
-
f"size ({settings.max_upload_size} bytes)"
|
|
330
|
-
)
|
|
331
|
-
|
|
332
|
-
async with get_async_session() as session:
|
|
333
|
-
# Validate board access
|
|
334
|
-
board_stmt = (
|
|
335
|
-
select(Boards).where(Boards.id == board_id).options(selectinload(Boards.board_members))
|
|
336
|
-
)
|
|
337
|
-
board = (await session.execute(board_stmt)).scalar_one_or_none()
|
|
338
|
-
|
|
339
|
-
if not board:
|
|
340
|
-
raise RuntimeError("Board not found")
|
|
341
|
-
|
|
342
|
-
# Check permissions (same as create_generation)
|
|
343
|
-
if not auth_context.user_id:
|
|
344
|
-
raise RuntimeError("User ID is required")
|
|
345
|
-
|
|
346
|
-
is_owner = board.owner_id == auth_context.user_id
|
|
347
|
-
is_editor = any(
|
|
348
|
-
m.user_id == auth_context.user_id and m.role in {"editor", "admin"}
|
|
349
|
-
for m in board.board_members
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
if not is_owner and not is_editor:
|
|
353
|
-
raise RuntimeError(
|
|
354
|
-
"Permission denied: You don't have permission to upload to this board"
|
|
355
|
-
)
|
|
356
|
-
|
|
357
|
-
# Create generation record (status=pending temporarily)
|
|
358
|
-
gen = Generations()
|
|
359
|
-
gen.tenant_id = auth_context.tenant_id
|
|
360
|
-
gen.board_id = board_id
|
|
361
|
-
gen.user_id = auth_context.user_id
|
|
362
|
-
gen.generator_name = f"user-upload-{artifact_type.value}"
|
|
363
|
-
gen.artifact_type = artifact_type.value
|
|
364
|
-
gen.status = "pending"
|
|
365
|
-
gen.progress = Decimal(0.0)
|
|
366
|
-
gen.input_params = {
|
|
367
|
-
"upload_source": upload_source,
|
|
368
|
-
"original_filename": filename,
|
|
369
|
-
"source_url": source_url,
|
|
370
|
-
"user_description": user_description,
|
|
371
|
-
}
|
|
372
|
-
gen.output_metadata = {
|
|
373
|
-
"file_size": len(file_content),
|
|
374
|
-
"mime_type": content_type,
|
|
375
|
-
"upload_timestamp": datetime.now(UTC).isoformat(),
|
|
376
|
-
}
|
|
377
|
-
# If parent_generation_id is provided, add it to input_artifacts
|
|
378
|
-
if parent_generation_id:
|
|
379
|
-
gen.input_artifacts = [
|
|
380
|
-
{
|
|
381
|
-
"generation_id": str(parent_generation_id),
|
|
382
|
-
"role": "parent",
|
|
383
|
-
"artifact_type": artifact_type.value,
|
|
384
|
-
}
|
|
385
|
-
]
|
|
386
|
-
else:
|
|
387
|
-
gen.input_artifacts = []
|
|
388
|
-
gen.started_at = datetime.now(UTC)
|
|
389
|
-
|
|
390
|
-
session.add(gen)
|
|
391
|
-
await session.flush() # Get ID
|
|
392
|
-
|
|
393
|
-
try:
|
|
394
|
-
# Upload to storage
|
|
395
|
-
storage_manager = create_storage_manager()
|
|
396
|
-
artifact_ref = await storage_manager.store_artifact(
|
|
397
|
-
artifact_id=str(gen.id),
|
|
398
|
-
content=file_content,
|
|
399
|
-
artifact_type=artifact_type.value,
|
|
400
|
-
content_type=content_type,
|
|
401
|
-
tenant_id=str(auth_context.tenant_id),
|
|
402
|
-
board_id=str(board_id),
|
|
403
|
-
)
|
|
404
|
-
|
|
405
|
-
# Update generation with storage info
|
|
406
|
-
gen.storage_url = artifact_ref.storage_url
|
|
407
|
-
gen.status = "completed"
|
|
408
|
-
gen.progress = Decimal(100.0)
|
|
409
|
-
gen.completed_at = datetime.now(UTC)
|
|
410
|
-
|
|
411
|
-
# Update metadata with storage details
|
|
412
|
-
if gen.output_metadata is None:
|
|
413
|
-
gen.output_metadata = {}
|
|
414
|
-
gen.output_metadata["storage_key"] = artifact_ref.storage_key
|
|
415
|
-
gen.output_metadata["storage_provider"] = artifact_ref.storage_provider
|
|
416
|
-
|
|
417
|
-
await session.commit()
|
|
418
|
-
await session.refresh(gen)
|
|
419
|
-
|
|
420
|
-
logger.info(
|
|
421
|
-
"Artifact uploaded",
|
|
422
|
-
generation_id=str(gen.id),
|
|
423
|
-
artifact_type=artifact_type,
|
|
424
|
-
file_size=len(file_content),
|
|
425
|
-
upload_source=upload_source,
|
|
426
|
-
)
|
|
427
|
-
|
|
428
|
-
# Convert to GraphQL type
|
|
429
|
-
return GenerationType(
|
|
430
|
-
id=gen.id,
|
|
431
|
-
tenant_id=gen.tenant_id,
|
|
432
|
-
board_id=gen.board_id,
|
|
433
|
-
user_id=gen.user_id,
|
|
434
|
-
generator_name=gen.generator_name,
|
|
435
|
-
artifact_type=ArtifactType(gen.artifact_type),
|
|
436
|
-
storage_url=gen.storage_url,
|
|
437
|
-
thumbnail_url=gen.thumbnail_url,
|
|
438
|
-
additional_files=gen.additional_files or [],
|
|
439
|
-
input_params=gen.input_params or {},
|
|
440
|
-
output_metadata=gen.output_metadata or {},
|
|
441
|
-
external_job_id=gen.external_job_id,
|
|
442
|
-
status=GenerationStatus(gen.status),
|
|
443
|
-
progress=float(gen.progress),
|
|
444
|
-
error_message=gen.error_message,
|
|
445
|
-
started_at=gen.started_at,
|
|
446
|
-
completed_at=gen.completed_at,
|
|
447
|
-
created_at=gen.created_at,
|
|
448
|
-
updated_at=gen.updated_at,
|
|
449
|
-
)
|
|
450
|
-
|
|
451
|
-
except Exception as e:
|
|
452
|
-
# Mark as failed
|
|
453
|
-
gen.status = "failed"
|
|
454
|
-
gen.error_message = str(e)
|
|
455
|
-
gen.completed_at = datetime.now(UTC)
|
|
456
|
-
await session.commit()
|
|
457
|
-
|
|
458
|
-
logger.error(
|
|
459
|
-
"Upload failed",
|
|
460
|
-
generation_id=str(gen.id),
|
|
461
|
-
error=str(e),
|
|
462
|
-
)
|
|
463
|
-
raise RuntimeError(f"Upload failed: {e}") from e
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import TYPE_CHECKING
|
|
4
|
-
|
|
5
|
-
import strawberry
|
|
6
|
-
|
|
7
|
-
if TYPE_CHECKING:
|
|
8
|
-
from ..types.board import Board
|
|
9
|
-
from ..types.user import User
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
async def resolve_current_user(info: strawberry.Info) -> User | None:
|
|
13
|
-
raise NotImplementedError
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
async def resolve_user_by_id(info: strawberry.Info, id: str) -> User | None:
|
|
17
|
-
raise NotImplementedError
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
async def resolve_user_boards(user: User, info: strawberry.Info) -> list[Board]:
|
|
21
|
-
raise NotImplementedError
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
async def resolve_user_member_boards(user: User, info: strawberry.Info) -> list[Board]:
|
|
25
|
-
raise NotImplementedError
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Main GraphQL schema definition using Strawberry
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
import strawberry
|
|
8
|
-
from fastapi import Request
|
|
9
|
-
from graphql import validate_schema as gql_validate_schema
|
|
10
|
-
from strawberry.fastapi import GraphQLRouter
|
|
11
|
-
|
|
12
|
-
from ..logging import get_logger
|
|
13
|
-
from .mutations.root import Mutation
|
|
14
|
-
from .queries.root import Query
|
|
15
|
-
|
|
16
|
-
# Import types to ensure they're registered with Strawberry
|
|
17
|
-
|
|
18
|
-
logger = get_logger(__name__)
|
|
19
|
-
|
|
20
|
-
# Create the GraphQL schema
|
|
21
|
-
schema = strawberry.Schema(
|
|
22
|
-
query=Query,
|
|
23
|
-
mutation=Mutation,
|
|
24
|
-
# Note: Introspection is enabled by default in strawberry
|
|
25
|
-
# TODO: Disable in production for security by using extensions
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def validate_schema() -> None:
|
|
30
|
-
"""Validate the GraphQL schema at startup.
|
|
31
|
-
|
|
32
|
-
This ensures that all type references can be resolved and catches
|
|
33
|
-
circular reference errors early, causing the server to fail fast
|
|
34
|
-
rather than returning 404s at runtime.
|
|
35
|
-
|
|
36
|
-
Raises:
|
|
37
|
-
Exception: If the schema is invalid or has unresolved types
|
|
38
|
-
"""
|
|
39
|
-
try:
|
|
40
|
-
# Convert to GraphQL core schema to trigger full validation
|
|
41
|
-
graphql_schema = schema._schema
|
|
42
|
-
|
|
43
|
-
# Validate the schema structure
|
|
44
|
-
errors = gql_validate_schema(graphql_schema)
|
|
45
|
-
if errors:
|
|
46
|
-
error_messages = [str(e) for e in errors]
|
|
47
|
-
raise Exception(f"GraphQL schema validation failed: {'; '.join(error_messages)}")
|
|
48
|
-
|
|
49
|
-
# Check that introspection query works (catches most resolution issues)
|
|
50
|
-
from graphql import get_introspection_query, graphql_sync
|
|
51
|
-
|
|
52
|
-
introspection_query = get_introspection_query()
|
|
53
|
-
result = graphql_sync(graphql_schema, introspection_query)
|
|
54
|
-
|
|
55
|
-
if result.errors:
|
|
56
|
-
error_messages = [str(e) for e in result.errors]
|
|
57
|
-
raise Exception(f"GraphQL introspection failed: {'; '.join(error_messages)}")
|
|
58
|
-
|
|
59
|
-
logger.info("GraphQL schema validation successful")
|
|
60
|
-
|
|
61
|
-
except Exception as e:
|
|
62
|
-
logger.error("GraphQL schema validation failed", error=str(e))
|
|
63
|
-
raise
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
# Create the GraphQL router for FastAPI integration
|
|
67
|
-
def create_graphql_router() -> GraphQLRouter[dict[str, Any], None]:
|
|
68
|
-
"""Create a GraphQL router for FastAPI."""
|
|
69
|
-
|
|
70
|
-
async def get_context(request: Request) -> dict[str, Any]:
|
|
71
|
-
"""Get the context for GraphQL resolvers."""
|
|
72
|
-
return {
|
|
73
|
-
"request": request,
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return GraphQLRouter(
|
|
77
|
-
schema,
|
|
78
|
-
path="/graphql",
|
|
79
|
-
graphiql=True, # Enable GraphiQL IDE in development
|
|
80
|
-
context_getter=get_context,
|
|
81
|
-
)
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Board GraphQL type definitions
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from datetime import datetime
|
|
6
|
-
from enum import Enum
|
|
7
|
-
from typing import TYPE_CHECKING, Annotated
|
|
8
|
-
from uuid import UUID
|
|
9
|
-
|
|
10
|
-
import strawberry
|
|
11
|
-
|
|
12
|
-
if TYPE_CHECKING:
|
|
13
|
-
from .generation import Generation
|
|
14
|
-
from .user import User
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@strawberry.enum
|
|
18
|
-
class BoardRole(Enum):
|
|
19
|
-
"""Board member role enumeration."""
|
|
20
|
-
|
|
21
|
-
VIEWER = "viewer"
|
|
22
|
-
EDITOR = "editor"
|
|
23
|
-
ADMIN = "admin"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@strawberry.type
|
|
27
|
-
class BoardMember:
|
|
28
|
-
"""Board member type for GraphQL API."""
|
|
29
|
-
|
|
30
|
-
id: UUID
|
|
31
|
-
board_id: UUID
|
|
32
|
-
user_id: UUID
|
|
33
|
-
role: BoardRole
|
|
34
|
-
invited_by: UUID | None
|
|
35
|
-
joined_at: datetime
|
|
36
|
-
|
|
37
|
-
@strawberry.field
|
|
38
|
-
async def user(self, info: strawberry.Info) -> Annotated["User", strawberry.lazy(".user")]:
|
|
39
|
-
"""Get the user for this board member."""
|
|
40
|
-
from ..resolvers.board import resolve_board_member_user
|
|
41
|
-
|
|
42
|
-
return await resolve_board_member_user(self, info)
|
|
43
|
-
|
|
44
|
-
@strawberry.field
|
|
45
|
-
async def inviter(
|
|
46
|
-
self, info: strawberry.Info
|
|
47
|
-
) -> Annotated["User", strawberry.lazy(".user")] | None: # noqa: E501
|
|
48
|
-
"""Get the user who invited this member."""
|
|
49
|
-
if not self.invited_by:
|
|
50
|
-
return None
|
|
51
|
-
from ..resolvers.board import resolve_board_member_inviter
|
|
52
|
-
|
|
53
|
-
return await resolve_board_member_inviter(self, info)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
@strawberry.type
|
|
57
|
-
class Board:
|
|
58
|
-
"""Board type for GraphQL API."""
|
|
59
|
-
|
|
60
|
-
id: UUID
|
|
61
|
-
tenant_id: UUID
|
|
62
|
-
owner_id: UUID
|
|
63
|
-
title: str
|
|
64
|
-
description: str | None
|
|
65
|
-
is_public: bool
|
|
66
|
-
settings: strawberry.scalars.JSON # type: ignore[reportInvalidTypeForm]
|
|
67
|
-
metadata: strawberry.scalars.JSON # type: ignore[reportInvalidTypeForm]
|
|
68
|
-
created_at: datetime
|
|
69
|
-
updated_at: datetime
|
|
70
|
-
|
|
71
|
-
@strawberry.field
|
|
72
|
-
async def owner(self, info: strawberry.Info) -> Annotated["User", strawberry.lazy(".user")]:
|
|
73
|
-
"""Get the owner of this board."""
|
|
74
|
-
from ..resolvers.board import resolve_board_owner
|
|
75
|
-
|
|
76
|
-
return await resolve_board_owner(self, info)
|
|
77
|
-
|
|
78
|
-
@strawberry.field
|
|
79
|
-
async def members(self, info: strawberry.Info) -> list[BoardMember]:
|
|
80
|
-
"""Get members of this board."""
|
|
81
|
-
from ..resolvers.board import resolve_board_members
|
|
82
|
-
|
|
83
|
-
return await resolve_board_members(self, info)
|
|
84
|
-
|
|
85
|
-
@strawberry.field
|
|
86
|
-
async def generations(
|
|
87
|
-
self,
|
|
88
|
-
info: strawberry.Info,
|
|
89
|
-
limit: int | None = 50,
|
|
90
|
-
offset: int | None = 0,
|
|
91
|
-
) -> list[Annotated["Generation", strawberry.lazy(".generation")]]:
|
|
92
|
-
"""Get generations in this board."""
|
|
93
|
-
from ..resolvers.board import resolve_board_generations
|
|
94
|
-
|
|
95
|
-
return await resolve_board_generations(self, info, limit or 50, offset or 0)
|
|
96
|
-
|
|
97
|
-
@strawberry.field
|
|
98
|
-
async def generation_count(self, info: strawberry.Info) -> int:
|
|
99
|
-
"""Get total number of generations in this board."""
|
|
100
|
-
from ..resolvers.board import resolve_board_generation_count
|
|
101
|
-
|
|
102
|
-
return await resolve_board_generation_count(self, info)
|