@weirdfingers/baseboards 0.2.0

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.
Files changed (139) hide show
  1. package/README.md +191 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +887 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +64 -0
  6. package/templates/README.md +120 -0
  7. package/templates/api/.env.example +62 -0
  8. package/templates/api/Dockerfile +32 -0
  9. package/templates/api/README.md +132 -0
  10. package/templates/api/alembic/env.py +106 -0
  11. package/templates/api/alembic/script.py.mako +28 -0
  12. package/templates/api/alembic/versions/20250101_000000_initial_schema.py +448 -0
  13. package/templates/api/alembic/versions/20251022_174729_remove_provider_name_from_generations.py +71 -0
  14. package/templates/api/alembic/versions/20251023_165852_switch_to_declarative_base_and_mapping.py +411 -0
  15. package/templates/api/alembic/versions/2025925_62735_add_seed_data_for_default_tenant.py +85 -0
  16. package/templates/api/alembic.ini +36 -0
  17. package/templates/api/config/generators.yaml +25 -0
  18. package/templates/api/config/storage_config.yaml +26 -0
  19. package/templates/api/docs/ADDING_GENERATORS.md +409 -0
  20. package/templates/api/docs/GENERATORS_API.md +502 -0
  21. package/templates/api/docs/MIGRATIONS.md +472 -0
  22. package/templates/api/docs/storage_providers.md +337 -0
  23. package/templates/api/pyproject.toml +165 -0
  24. package/templates/api/src/boards/__init__.py +10 -0
  25. package/templates/api/src/boards/api/app.py +171 -0
  26. package/templates/api/src/boards/api/auth.py +75 -0
  27. package/templates/api/src/boards/api/endpoints/__init__.py +3 -0
  28. package/templates/api/src/boards/api/endpoints/jobs.py +76 -0
  29. package/templates/api/src/boards/api/endpoints/setup.py +505 -0
  30. package/templates/api/src/boards/api/endpoints/sse.py +129 -0
  31. package/templates/api/src/boards/api/endpoints/storage.py +74 -0
  32. package/templates/api/src/boards/api/endpoints/tenant_registration.py +296 -0
  33. package/templates/api/src/boards/api/endpoints/webhooks.py +13 -0
  34. package/templates/api/src/boards/auth/__init__.py +15 -0
  35. package/templates/api/src/boards/auth/adapters/__init__.py +20 -0
  36. package/templates/api/src/boards/auth/adapters/auth0.py +220 -0
  37. package/templates/api/src/boards/auth/adapters/base.py +73 -0
  38. package/templates/api/src/boards/auth/adapters/clerk.py +172 -0
  39. package/templates/api/src/boards/auth/adapters/jwt.py +122 -0
  40. package/templates/api/src/boards/auth/adapters/none.py +102 -0
  41. package/templates/api/src/boards/auth/adapters/oidc.py +284 -0
  42. package/templates/api/src/boards/auth/adapters/supabase.py +110 -0
  43. package/templates/api/src/boards/auth/context.py +35 -0
  44. package/templates/api/src/boards/auth/factory.py +115 -0
  45. package/templates/api/src/boards/auth/middleware.py +221 -0
  46. package/templates/api/src/boards/auth/provisioning.py +129 -0
  47. package/templates/api/src/boards/auth/tenant_extraction.py +278 -0
  48. package/templates/api/src/boards/cli.py +354 -0
  49. package/templates/api/src/boards/config.py +116 -0
  50. package/templates/api/src/boards/database/__init__.py +7 -0
  51. package/templates/api/src/boards/database/cli.py +110 -0
  52. package/templates/api/src/boards/database/connection.py +252 -0
  53. package/templates/api/src/boards/database/models.py +19 -0
  54. package/templates/api/src/boards/database/seed_data.py +182 -0
  55. package/templates/api/src/boards/dbmodels/__init__.py +455 -0
  56. package/templates/api/src/boards/generators/__init__.py +57 -0
  57. package/templates/api/src/boards/generators/artifacts.py +53 -0
  58. package/templates/api/src/boards/generators/base.py +140 -0
  59. package/templates/api/src/boards/generators/implementations/__init__.py +12 -0
  60. package/templates/api/src/boards/generators/implementations/audio/__init__.py +3 -0
  61. package/templates/api/src/boards/generators/implementations/audio/whisper.py +66 -0
  62. package/templates/api/src/boards/generators/implementations/image/__init__.py +3 -0
  63. package/templates/api/src/boards/generators/implementations/image/dalle3.py +93 -0
  64. package/templates/api/src/boards/generators/implementations/image/flux_pro.py +85 -0
  65. package/templates/api/src/boards/generators/implementations/video/__init__.py +3 -0
  66. package/templates/api/src/boards/generators/implementations/video/lipsync.py +70 -0
  67. package/templates/api/src/boards/generators/loader.py +253 -0
  68. package/templates/api/src/boards/generators/registry.py +114 -0
  69. package/templates/api/src/boards/generators/resolution.py +515 -0
  70. package/templates/api/src/boards/generators/testmods/class_gen.py +34 -0
  71. package/templates/api/src/boards/generators/testmods/import_side_effect.py +35 -0
  72. package/templates/api/src/boards/graphql/__init__.py +7 -0
  73. package/templates/api/src/boards/graphql/access_control.py +136 -0
  74. package/templates/api/src/boards/graphql/mutations/root.py +136 -0
  75. package/templates/api/src/boards/graphql/queries/root.py +116 -0
  76. package/templates/api/src/boards/graphql/resolvers/__init__.py +8 -0
  77. package/templates/api/src/boards/graphql/resolvers/auth.py +12 -0
  78. package/templates/api/src/boards/graphql/resolvers/board.py +1055 -0
  79. package/templates/api/src/boards/graphql/resolvers/generation.py +889 -0
  80. package/templates/api/src/boards/graphql/resolvers/generator.py +50 -0
  81. package/templates/api/src/boards/graphql/resolvers/user.py +25 -0
  82. package/templates/api/src/boards/graphql/schema.py +81 -0
  83. package/templates/api/src/boards/graphql/types/board.py +102 -0
  84. package/templates/api/src/boards/graphql/types/generation.py +130 -0
  85. package/templates/api/src/boards/graphql/types/generator.py +17 -0
  86. package/templates/api/src/boards/graphql/types/user.py +47 -0
  87. package/templates/api/src/boards/jobs/repository.py +104 -0
  88. package/templates/api/src/boards/logging.py +195 -0
  89. package/templates/api/src/boards/middleware.py +339 -0
  90. package/templates/api/src/boards/progress/__init__.py +4 -0
  91. package/templates/api/src/boards/progress/models.py +25 -0
  92. package/templates/api/src/boards/progress/publisher.py +64 -0
  93. package/templates/api/src/boards/py.typed +0 -0
  94. package/templates/api/src/boards/redis_pool.py +118 -0
  95. package/templates/api/src/boards/storage/__init__.py +52 -0
  96. package/templates/api/src/boards/storage/base.py +363 -0
  97. package/templates/api/src/boards/storage/config.py +187 -0
  98. package/templates/api/src/boards/storage/factory.py +278 -0
  99. package/templates/api/src/boards/storage/implementations/__init__.py +27 -0
  100. package/templates/api/src/boards/storage/implementations/gcs.py +340 -0
  101. package/templates/api/src/boards/storage/implementations/local.py +201 -0
  102. package/templates/api/src/boards/storage/implementations/s3.py +294 -0
  103. package/templates/api/src/boards/storage/implementations/supabase.py +218 -0
  104. package/templates/api/src/boards/tenant_isolation.py +446 -0
  105. package/templates/api/src/boards/validation.py +262 -0
  106. package/templates/api/src/boards/workers/__init__.py +1 -0
  107. package/templates/api/src/boards/workers/actors.py +201 -0
  108. package/templates/api/src/boards/workers/cli.py +125 -0
  109. package/templates/api/src/boards/workers/context.py +188 -0
  110. package/templates/api/src/boards/workers/middleware.py +58 -0
  111. package/templates/api/src/py.typed +0 -0
  112. package/templates/compose.dev.yaml +39 -0
  113. package/templates/compose.yaml +109 -0
  114. package/templates/docker/env.example +23 -0
  115. package/templates/web/.env.example +28 -0
  116. package/templates/web/Dockerfile +51 -0
  117. package/templates/web/components.json +22 -0
  118. package/templates/web/imageLoader.js +18 -0
  119. package/templates/web/next-env.d.ts +5 -0
  120. package/templates/web/next.config.js +36 -0
  121. package/templates/web/package.json +37 -0
  122. package/templates/web/postcss.config.mjs +7 -0
  123. package/templates/web/public/favicon.ico +0 -0
  124. package/templates/web/src/app/boards/[boardId]/page.tsx +232 -0
  125. package/templates/web/src/app/globals.css +120 -0
  126. package/templates/web/src/app/layout.tsx +21 -0
  127. package/templates/web/src/app/page.tsx +35 -0
  128. package/templates/web/src/app/providers.tsx +18 -0
  129. package/templates/web/src/components/boards/ArtifactInputSlots.tsx +142 -0
  130. package/templates/web/src/components/boards/ArtifactPreview.tsx +125 -0
  131. package/templates/web/src/components/boards/GenerationGrid.tsx +45 -0
  132. package/templates/web/src/components/boards/GenerationInput.tsx +251 -0
  133. package/templates/web/src/components/boards/GeneratorSelector.tsx +89 -0
  134. package/templates/web/src/components/header.tsx +30 -0
  135. package/templates/web/src/components/ui/button.tsx +58 -0
  136. package/templates/web/src/components/ui/card.tsx +92 -0
  137. package/templates/web/src/components/ui/navigation-menu.tsx +168 -0
  138. package/templates/web/src/lib/utils.ts +6 -0
  139. package/templates/web/tsconfig.json +47 -0
@@ -0,0 +1,50 @@
1
+ """
2
+ Generator resolvers for GraphQL API
3
+ """
4
+
5
+ import strawberry
6
+
7
+ from ...generators.registry import registry
8
+ from ..types.generation import ArtifactType
9
+ from ..types.generator import GeneratorInfo
10
+
11
+
12
+ async def resolve_generators(
13
+ info: strawberry.Info,
14
+ artifact_type: str | None = None,
15
+ ) -> list[GeneratorInfo]:
16
+ """Get all available generators, optionally filtered by artifact type.
17
+
18
+ Args:
19
+ info: GraphQL info context
20
+ artifact_type: Optional filter by artifact type (image, video, audio, text)
21
+
22
+ Returns:
23
+ List of generator information
24
+ """
25
+ _ = info # Unused but required by GraphQL interface
26
+
27
+ # Get generators from registry
28
+ if artifact_type:
29
+ generators = registry.list_by_artifact_type(artifact_type)
30
+ else:
31
+ generators = registry.list_all()
32
+
33
+ # Convert to GraphQL types
34
+ result = []
35
+ for gen in generators:
36
+ input_schema_class = gen.get_input_schema()
37
+
38
+ # Convert string artifact_type to enum
39
+ artifact_type_enum = ArtifactType(gen.artifact_type)
40
+
41
+ result.append(
42
+ GeneratorInfo(
43
+ name=gen.name,
44
+ description=gen.description,
45
+ artifact_type=artifact_type_enum,
46
+ input_schema=input_schema_class.model_json_schema(),
47
+ )
48
+ )
49
+
50
+ return result
@@ -0,0 +1,25 @@
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
@@ -0,0 +1,81 @@
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
+ )
@@ -0,0 +1,102 @@
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)
@@ -0,0 +1,130 @@
1
+ """
2
+ Generation 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 .board import Board
14
+ from .user import User
15
+
16
+
17
+ @strawberry.enum
18
+ class ArtifactType(Enum):
19
+ """Artifact type enumeration."""
20
+
21
+ IMAGE = "image"
22
+ VIDEO = "video"
23
+ AUDIO = "audio"
24
+ TEXT = "text"
25
+ LORA = "lora"
26
+ MODEL = "model"
27
+
28
+
29
+ @strawberry.enum
30
+ class GenerationStatus(Enum):
31
+ """Generation status enumeration."""
32
+
33
+ PENDING = "pending"
34
+ PROCESSING = "processing"
35
+ COMPLETED = "completed"
36
+ FAILED = "failed"
37
+ CANCELLED = "cancelled"
38
+
39
+
40
+ @strawberry.type
41
+ class AdditionalFile:
42
+ """Additional file associated with a generation."""
43
+
44
+ url: str
45
+ type: str
46
+ metadata: strawberry.scalars.JSON # type: ignore[reportInvalidTypeForm]
47
+
48
+
49
+ @strawberry.type
50
+ class Generation:
51
+ """Generation type for GraphQL API."""
52
+
53
+ id: UUID
54
+ tenant_id: UUID
55
+ board_id: UUID
56
+ user_id: UUID
57
+
58
+ # Generation details
59
+ generator_name: str
60
+ artifact_type: ArtifactType
61
+
62
+ # Storage
63
+ storage_url: str | None
64
+ thumbnail_url: str | None
65
+ additional_files: list[AdditionalFile]
66
+
67
+ # Parameters and metadata
68
+ input_params: strawberry.scalars.JSON # type: ignore[reportInvalidTypeForm]
69
+ output_metadata: strawberry.scalars.JSON # type: ignore[reportInvalidTypeForm]
70
+
71
+ # Lineage
72
+ parent_generation_id: UUID | None
73
+ input_generation_ids: list[UUID]
74
+
75
+ # Job tracking
76
+ external_job_id: str | None
77
+ status: GenerationStatus
78
+ progress: float
79
+ error_message: str | None
80
+
81
+ # Timestamps
82
+ started_at: datetime | None
83
+ completed_at: datetime | None
84
+ created_at: datetime
85
+ updated_at: datetime
86
+
87
+ @strawberry.field
88
+ async def board(self, info: strawberry.Info) -> Annotated["Board", strawberry.lazy(".board")]:
89
+ """Get the board this generation belongs to."""
90
+ from ..resolvers.generation import resolve_generation_board
91
+
92
+ return await resolve_generation_board(self, info)
93
+
94
+ @strawberry.field
95
+ async def user(self, info: strawberry.Info) -> Annotated["User", strawberry.lazy(".user")]:
96
+ """Get the user who created this generation."""
97
+ from ..resolvers.generation import resolve_generation_user
98
+
99
+ return await resolve_generation_user(self, info)
100
+
101
+ @strawberry.field
102
+ async def parent(
103
+ self, info: strawberry.Info
104
+ ) -> Annotated["Generation", strawberry.lazy(".generation")] | None: # noqa: E501
105
+ """Get the parent generation if any."""
106
+ if not self.parent_generation_id:
107
+ return None
108
+ from ..resolvers.generation import resolve_generation_parent
109
+
110
+ return await resolve_generation_parent(self, info)
111
+
112
+ @strawberry.field
113
+ async def inputs(
114
+ self, info: strawberry.Info
115
+ ) -> list[Annotated["Generation", strawberry.lazy(".generation")]]: # noqa: E501
116
+ """Get input generations used for this generation."""
117
+ if not self.input_generation_ids:
118
+ return []
119
+ from ..resolvers.generation import resolve_generation_inputs
120
+
121
+ return await resolve_generation_inputs(self, info)
122
+
123
+ @strawberry.field
124
+ async def children(
125
+ self, info: strawberry.Info
126
+ ) -> list[Annotated["Generation", strawberry.lazy(".generation")]]: # noqa: E501
127
+ """Get child generations derived from this one."""
128
+ from ..resolvers.generation import resolve_generation_children
129
+
130
+ return await resolve_generation_children(self, info)
@@ -0,0 +1,17 @@
1
+ """
2
+ Generator GraphQL type definitions
3
+ """
4
+
5
+ import strawberry
6
+
7
+ from .generation import ArtifactType
8
+
9
+
10
+ @strawberry.type
11
+ class GeneratorInfo:
12
+ """Information about an available generator."""
13
+
14
+ name: str
15
+ description: str
16
+ artifact_type: ArtifactType
17
+ input_schema: strawberry.scalars.JSON # type: ignore[reportInvalidTypeForm]
@@ -0,0 +1,47 @@
1
+ """
2
+ User GraphQL type definitions
3
+ """
4
+
5
+ from datetime import datetime
6
+ from typing import TYPE_CHECKING, Annotated
7
+ from uuid import UUID
8
+
9
+ import strawberry
10
+
11
+ if TYPE_CHECKING:
12
+ from .board import Board
13
+
14
+
15
+ @strawberry.type
16
+ class User:
17
+ """User type for GraphQL API."""
18
+
19
+ id: UUID
20
+ tenant_id: UUID
21
+ auth_provider: str
22
+ auth_subject: str
23
+ email: str | None
24
+ display_name: str | None
25
+ avatar_url: str | None
26
+ created_at: datetime
27
+ updated_at: datetime
28
+
29
+ @strawberry.field
30
+ async def boards(
31
+ self, info: strawberry.Info
32
+ ) -> list[Annotated["Board", strawberry.lazy(".board")]]: # noqa: E501
33
+ """Get boards owned by this user."""
34
+ # TODO: Implement data loader
35
+ from ..resolvers.user import resolve_user_boards
36
+
37
+ return await resolve_user_boards(self, info)
38
+
39
+ @strawberry.field
40
+ async def member_boards(
41
+ self, info: strawberry.Info
42
+ ) -> list[Annotated["Board", strawberry.lazy(".board")]]: # noqa: E501
43
+ """Get boards where user is a member."""
44
+ # TODO: Implement data loader
45
+ from ..resolvers.user import resolve_user_member_boards
46
+
47
+ return await resolve_user_member_boards(self, info)
@@ -0,0 +1,104 @@
1
+ """Repository helpers for Generations job lifecycle."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime
6
+ from decimal import Decimal
7
+ from typing import Any
8
+ from uuid import UUID
9
+
10
+ from sqlalchemy import select, update
11
+ from sqlalchemy.ext.asyncio import AsyncSession
12
+
13
+ from ..dbmodels import Generations
14
+
15
+
16
+ async def get_generation(session: AsyncSession, generation_id: str | UUID) -> Generations:
17
+ stmt = select(Generations).where(Generations.id == str(generation_id))
18
+ res = await session.execute(stmt)
19
+ row = res.scalar_one()
20
+ return row
21
+
22
+
23
+ async def update_progress(
24
+ session: AsyncSession,
25
+ generation_id: str | UUID,
26
+ *,
27
+ status: str,
28
+ progress: float,
29
+ error_message: str | None = None,
30
+ ) -> None:
31
+ now = datetime.now(UTC)
32
+ stmt = (
33
+ update(Generations)
34
+ .where(Generations.id == str(generation_id))
35
+ .values(
36
+ status=status,
37
+ progress=progress,
38
+ error_message=error_message,
39
+ updated_at=now,
40
+ started_at=now if status == "processing" else Generations.started_at,
41
+ completed_at=(now if status in {"completed", "failed", "cancelled"} else None),
42
+ )
43
+ )
44
+ await session.execute(stmt)
45
+
46
+
47
+ async def create_generation(
48
+ session: AsyncSession,
49
+ *,
50
+ tenant_id: UUID,
51
+ board_id: UUID,
52
+ user_id: UUID,
53
+ generator_name: str,
54
+ artifact_type: str,
55
+ input_params: dict,
56
+ ) -> Generations:
57
+ gen = Generations()
58
+ gen.tenant_id = tenant_id
59
+ gen.board_id = board_id
60
+ gen.user_id = user_id
61
+ gen.generator_name = generator_name
62
+ gen.artifact_type = artifact_type
63
+ gen.input_params = input_params
64
+ gen.status = "pending"
65
+ gen.progress = Decimal(0.0)
66
+ session.add(gen)
67
+ await session.flush()
68
+ return gen
69
+
70
+
71
+ async def set_external_job_id(
72
+ session: AsyncSession, generation_id: str | UUID, external_job_id: str
73
+ ) -> None:
74
+ stmt = (
75
+ update(Generations)
76
+ .where(Generations.id == str(generation_id))
77
+ .values(external_job_id=external_job_id)
78
+ )
79
+ await session.execute(stmt)
80
+
81
+
82
+ async def finalize_success(
83
+ session: AsyncSession,
84
+ generation_id: str | UUID,
85
+ *,
86
+ storage_url: str | None = None,
87
+ thumbnail_url: str | None = None,
88
+ output_metadata: dict[str, Any] | None = None,
89
+ ) -> None:
90
+ now = datetime.now(UTC)
91
+ stmt = (
92
+ update(Generations)
93
+ .where(Generations.id == str(generation_id))
94
+ .values(
95
+ status="completed",
96
+ progress=100.0,
97
+ storage_url=storage_url,
98
+ thumbnail_url=thumbnail_url,
99
+ output_metadata=output_metadata or {},
100
+ updated_at=now,
101
+ completed_at=now,
102
+ )
103
+ )
104
+ await session.execute(stmt)