@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,136 @@
1
+ """
2
+ Shared access control logic for GraphQL resolvers
3
+ """
4
+
5
+ from enum import Enum
6
+ from typing import TYPE_CHECKING
7
+
8
+ import strawberry
9
+
10
+ from ..auth.middleware import get_auth_context_optional
11
+ from ..logging import get_logger
12
+
13
+ if TYPE_CHECKING:
14
+ from ..auth.context import AuthContext
15
+ from ..dbmodels import Boards
16
+
17
+ logger = get_logger(__name__)
18
+
19
+
20
+ @strawberry.enum
21
+ class BoardQueryRole(Enum):
22
+ """Role filter for board queries"""
23
+
24
+ ANY = "any"
25
+ OWNER = "owner"
26
+ MEMBER = "member"
27
+
28
+
29
+ @strawberry.enum
30
+ class SortOrder(Enum):
31
+ """Sort order for queries"""
32
+
33
+ CREATED_ASC = "created_asc"
34
+ CREATED_DESC = "created_desc"
35
+ UPDATED_ASC = "updated_asc"
36
+ UPDATED_DESC = "updated_desc"
37
+
38
+
39
+ async def get_auth_context_from_info(info: strawberry.Info) -> "AuthContext | None":
40
+ """
41
+ Extract auth context from GraphQL info object.
42
+
43
+ Returns None if request is not available or auth fails.
44
+ """
45
+ request = info.context.get("request")
46
+ if not request:
47
+ logger.error("Request not found in GraphQL context")
48
+ return None
49
+
50
+ return await get_auth_context_optional(
51
+ authorization=request.headers.get("authorization"),
52
+ x_tenant=request.headers.get("x-tenant"),
53
+ )
54
+
55
+
56
+ def can_access_board(board: "Boards", auth_context: "AuthContext | None") -> bool:
57
+ """
58
+ Check if a user can access a board based on authorization rules.
59
+
60
+ Board is accessible if:
61
+ 1. It's public
62
+ 2. User is the owner
63
+ 3. User is a member
64
+
65
+ Args:
66
+ board: The board to check access for
67
+ auth_context: The authentication context (can be None)
68
+
69
+ Returns:
70
+ True if access is allowed, False otherwise
71
+ """
72
+ # Public boards are accessible to everyone
73
+ if board.is_public:
74
+ return True
75
+
76
+ # Private boards require authentication
77
+ if not auth_context or not auth_context.is_authenticated:
78
+ return False
79
+
80
+ # Owner has access
81
+ if board.owner_id == auth_context.user_id:
82
+ return True
83
+
84
+ # Check if user is a member
85
+ return any(member.user_id == auth_context.user_id for member in board.board_members)
86
+
87
+
88
+ def can_access_board_details(board: "Boards", auth_context: "AuthContext | None") -> bool:
89
+ """
90
+ Check if a user can access detailed board information (members, owner, etc).
91
+
92
+ Currently same as can_access_board, but separated for future customization.
93
+ """
94
+ return can_access_board(board, auth_context)
95
+
96
+
97
+ def is_board_owner_or_member(board: "Boards", auth_context: "AuthContext | None") -> bool:
98
+ """
99
+ Check if the authenticated user is the owner or a member of the board.
100
+
101
+ This is stricter than can_access_board - public access is not sufficient.
102
+ """
103
+ if not auth_context or not auth_context.is_authenticated:
104
+ return False
105
+
106
+ # Owner has access
107
+ if board.owner_id == auth_context.user_id:
108
+ return True
109
+
110
+ # Check if user is a member
111
+ return any(member.user_id == auth_context.user_id for member in board.board_members)
112
+
113
+
114
+ def ensure_preloaded(obj, attr_name: str, error_msg: str | None = None) -> None:
115
+ """
116
+ Ensure that a relationship attribute has been preloaded.
117
+
118
+ Args:
119
+ obj: The SQLAlchemy object
120
+ attr_name: Name of the relationship attribute
121
+ error_msg: Custom error message (optional)
122
+
123
+ Raises:
124
+ RuntimeError: If the attribute was not preloaded
125
+ """
126
+ try:
127
+ # Try to access the attribute
128
+ getattr(obj, attr_name)
129
+ except Exception as e:
130
+ if "was not loaded" in str(e) or "lazy loading" in str(e):
131
+ msg = error_msg or (
132
+ f"Relationship '{attr_name}' was not preloaded. " "Use selectinload() in the query."
133
+ )
134
+ raise RuntimeError(msg) from e
135
+ # Re-raise other exceptions
136
+ raise
@@ -0,0 +1,136 @@
1
+ """
2
+ Root GraphQL mutation definitions
3
+ """
4
+
5
+ from uuid import UUID
6
+
7
+ import strawberry
8
+
9
+ from ..types.board import Board, BoardRole
10
+ from ..types.generation import ArtifactType, Generation
11
+
12
+
13
+ # Input types for mutations
14
+ @strawberry.input
15
+ class CreateBoardInput:
16
+ """Input for creating a new board."""
17
+
18
+ title: str
19
+ description: str | None = None
20
+ is_public: bool = False
21
+ settings: strawberry.scalars.JSON | None = None # type: ignore[reportInvalidTypeForm]
22
+
23
+
24
+ @strawberry.input
25
+ class UpdateBoardInput:
26
+ """Input for updating a board."""
27
+
28
+ id: UUID
29
+ title: str | None = None
30
+ description: str | None = None
31
+ is_public: bool | None = None
32
+ settings: strawberry.scalars.JSON | None = None # type: ignore[reportInvalidTypeForm]
33
+
34
+
35
+ @strawberry.input
36
+ class AddBoardMemberInput:
37
+ """Input for adding a board member."""
38
+
39
+ board_id: UUID
40
+ user_id: UUID
41
+ role: BoardRole = BoardRole.VIEWER
42
+
43
+
44
+ @strawberry.input
45
+ class CreateGenerationInput:
46
+ """Input for creating a new generation."""
47
+
48
+ board_id: UUID
49
+ generator_name: str
50
+ artifact_type: ArtifactType
51
+ input_params: strawberry.scalars.JSON # type: ignore[reportInvalidTypeForm]
52
+ parent_generation_id: UUID | None = None
53
+ input_generation_ids: list[UUID] | None = None
54
+
55
+
56
+ @strawberry.type
57
+ class Mutation:
58
+ """Root GraphQL mutation type."""
59
+
60
+ # Board mutations
61
+ @strawberry.mutation(name="createBoard")
62
+ async def create_board(self, info: strawberry.Info, input: CreateBoardInput) -> Board:
63
+ """Create a new board."""
64
+ from ..resolvers.board import create_board
65
+
66
+ return await create_board(info, input)
67
+
68
+ @strawberry.mutation(name="updateBoard")
69
+ async def update_board(self, info: strawberry.Info, input: UpdateBoardInput) -> Board:
70
+ """Update an existing board."""
71
+ from ..resolvers.board import update_board
72
+
73
+ return await update_board(info, input)
74
+
75
+ @strawberry.mutation(name="deleteBoard")
76
+ async def delete_board(self, info: strawberry.Info, id: UUID) -> bool:
77
+ """Delete a board."""
78
+ from ..resolvers.board import delete_board
79
+
80
+ return await delete_board(info, id)
81
+
82
+ @strawberry.mutation(name="addBoardMember")
83
+ async def add_board_member(self, info: strawberry.Info, input: AddBoardMemberInput) -> Board:
84
+ """Add a member to a board."""
85
+ from ..resolvers.board import add_board_member
86
+
87
+ return await add_board_member(info, input)
88
+
89
+ @strawberry.mutation(name="removeBoardMember")
90
+ async def remove_board_member(
91
+ self, info: strawberry.Info, board_id: UUID, user_id: UUID
92
+ ) -> Board:
93
+ """Remove a member from a board."""
94
+ from ..resolvers.board import remove_board_member
95
+
96
+ return await remove_board_member(info, board_id, user_id)
97
+
98
+ @strawberry.mutation(name="updateBoardMemberRole")
99
+ async def update_board_member_role(
100
+ self, info: strawberry.Info, board_id: UUID, user_id: UUID, role: BoardRole
101
+ ) -> Board:
102
+ """Update a board member's role."""
103
+ from ..resolvers.board import update_board_member_role
104
+
105
+ return await update_board_member_role(info, board_id, user_id, role)
106
+
107
+ # Generation mutations
108
+ @strawberry.mutation(name="createGeneration")
109
+ async def create_generation(
110
+ self, info: strawberry.Info, input: CreateGenerationInput
111
+ ) -> Generation:
112
+ """Create a new generation (start a job)."""
113
+ from ..resolvers.generation import create_generation
114
+
115
+ return await create_generation(info, input)
116
+
117
+ @strawberry.mutation(name="cancelGeneration")
118
+ async def cancel_generation(self, info: strawberry.Info, id: UUID) -> Generation:
119
+ """Cancel a pending or processing generation."""
120
+ from ..resolvers.generation import cancel_generation
121
+
122
+ return await cancel_generation(info, id)
123
+
124
+ @strawberry.mutation(name="deleteGeneration")
125
+ async def delete_generation(self, info: strawberry.Info, id: UUID) -> bool:
126
+ """Delete a generation."""
127
+ from ..resolvers.generation import delete_generation
128
+
129
+ return await delete_generation(info, id)
130
+
131
+ @strawberry.mutation
132
+ async def regenerate(self, info: strawberry.Info, id: UUID) -> Generation:
133
+ """Regenerate from an existing generation."""
134
+ from ..resolvers.generation import regenerate
135
+
136
+ return await regenerate(info, id)
@@ -0,0 +1,116 @@
1
+ """
2
+ Root GraphQL query definitions
3
+ """
4
+
5
+ from uuid import UUID
6
+
7
+ import strawberry
8
+
9
+ from ..access_control import BoardQueryRole, SortOrder
10
+ from ..types.board import Board
11
+ from ..types.generation import ArtifactType, Generation, GenerationStatus
12
+ from ..types.generator import GeneratorInfo
13
+ from ..types.user import User
14
+
15
+
16
+ @strawberry.type
17
+ class Query:
18
+ """Root GraphQL query type."""
19
+
20
+ @strawberry.field
21
+ async def me(self, info: strawberry.Info) -> User | None:
22
+ """Get the current authenticated user."""
23
+ from ..resolvers.auth import resolve_current_user
24
+
25
+ return await resolve_current_user(info)
26
+
27
+ @strawberry.field
28
+ async def user(self, info: strawberry.Info, id: UUID) -> User | None:
29
+ """Get a user by ID."""
30
+ from ..resolvers.user import resolve_user_by_id
31
+
32
+ return await resolve_user_by_id(info, str(id))
33
+
34
+ @strawberry.field
35
+ async def board(self, info: strawberry.Info, id: UUID) -> Board | None:
36
+ """Get a board by ID."""
37
+ from ..resolvers.board import resolve_board_by_id
38
+
39
+ return await resolve_board_by_id(info, id)
40
+
41
+ @strawberry.field
42
+ async def my_boards(
43
+ self,
44
+ info: strawberry.Info,
45
+ limit: int | None = 50,
46
+ offset: int | None = 0,
47
+ role: BoardQueryRole | None = None,
48
+ sort: SortOrder | None = None,
49
+ ) -> list[Board]:
50
+ """Get boards owned by or shared with the current user."""
51
+ from ..resolvers.board import resolve_my_boards
52
+
53
+ return await resolve_my_boards(
54
+ info,
55
+ limit or 50,
56
+ offset or 0,
57
+ role or BoardQueryRole.ANY,
58
+ sort or SortOrder.UPDATED_DESC,
59
+ )
60
+
61
+ @strawberry.field
62
+ async def public_boards(
63
+ self,
64
+ info: strawberry.Info,
65
+ limit: int | None = 50,
66
+ offset: int | None = 0,
67
+ sort: SortOrder | None = None,
68
+ ) -> list[Board]:
69
+ """Get public boards."""
70
+ from ..resolvers.board import resolve_public_boards
71
+
72
+ return await resolve_public_boards(
73
+ info, limit or 50, offset or 0, sort or SortOrder.UPDATED_DESC
74
+ )
75
+
76
+ @strawberry.field
77
+ async def generation(self, info: strawberry.Info, id: UUID) -> Generation | None:
78
+ """Get a generation by ID."""
79
+ from ..resolvers.generation import resolve_generation_by_id
80
+
81
+ return await resolve_generation_by_id(info, id)
82
+
83
+ @strawberry.field
84
+ async def recent_generations(
85
+ self,
86
+ info: strawberry.Info,
87
+ board_id: UUID | None = None,
88
+ status: GenerationStatus | None = None,
89
+ artifact_type: ArtifactType | None = None,
90
+ limit: int | None = 50,
91
+ offset: int | None = 0,
92
+ ) -> list[Generation]:
93
+ """Get recent generations with optional filters."""
94
+ from ..resolvers.generation import resolve_recent_generations
95
+
96
+ return await resolve_recent_generations(
97
+ info, board_id, status, artifact_type, limit or 50, offset or 0
98
+ )
99
+
100
+ @strawberry.field
101
+ async def search_boards(
102
+ self, info: strawberry.Info, query: str, limit: int | None = 50, offset: int | None = 0
103
+ ) -> list[Board]:
104
+ """Search for boards by title or description."""
105
+ from ..resolvers.board import search_boards
106
+
107
+ return await search_boards(info, query, limit or 50, offset or 0)
108
+
109
+ @strawberry.field
110
+ async def generators(
111
+ self, info: strawberry.Info, artifact_type: str | None = None
112
+ ) -> list[GeneratorInfo]:
113
+ """Get all available generators, optionally filtered by artifact type."""
114
+ from ..resolvers.generator import resolve_generators
115
+
116
+ return await resolve_generators(info, artifact_type)
@@ -0,0 +1,8 @@
1
+ """Resolver package for GraphQL schema.
2
+
3
+ This package provides resolver function stubs referenced by the GraphQL types,
4
+ queries, and mutations. Implementations should be provided to connect to the
5
+ database and application services.
6
+ """
7
+
8
+ # Intentionally empty; functions are defined in sibling modules.
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import strawberry
6
+
7
+ if TYPE_CHECKING:
8
+ from ..types.user import User
9
+
10
+
11
+ async def resolve_current_user(info: strawberry.Info) -> User | None:
12
+ raise NotImplementedError