Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:29:15 +08:00
commit be476a3fea
76 changed files with 12812 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
{
"root": true,
"env": { "browser": true, "es2020": true },
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended"
],
"parser": "@typescript-eslint/parser",
"plugins": ["react-refresh"],
"rules": {
"react-hooks/exhaustive-deps": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "off",
"no-unused-vars": "off",
"react-refresh/only-export-components": "off"
}
}

View File

@@ -0,0 +1,9 @@
{
"tabWidth": 2,
"semi": true,
"printWidth": 90,
"singleQuote": false,
"endOfLine": "lf",
"trailingComma": "all",
"plugins": ["prettier-plugin-organize-imports", "prettier-plugin-tailwindcss"]
}

View File

@@ -0,0 +1,186 @@
"""Example FastAPI Router Template.
Copy and adapt this for new Grey Haven API endpoints.
"""
from datetime import datetime
from typing import Annotated
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel, Field
# Replace with your actual imports
from app.db.models.example import ExampleDB
from app.db.repositories.example_repository import ExampleRepository
from app.dependencies import get_current_user, get_example_repository
from app.schemas.example import ExampleCreate, ExampleResponse, ExampleUpdate
# 1. Create router with tags and dependencies
router = APIRouter(
prefix="/examples",
tags=["examples"],
dependencies=[Depends(get_current_user)], # Require authentication
)
# 2. POST endpoint - Create resource
@router.post("/", response_model=ExampleResponse, status_code=status.HTTP_201_CREATED)
async def create_example(
data: ExampleCreate,
repo: Annotated[ExampleRepository, Depends(get_example_repository)],
current_user: Annotated[dict, Depends(get_current_user)],
) -> ExampleResponse:
"""
Create a new example resource.
Args:
data: Example creation data
repo: Example repository dependency
current_user: Currently authenticated user
Returns:
ExampleResponse: Created example
Raises:
HTTPException: If creation fails
"""
# Create resource with tenant isolation
example = await repo.create(data, tenant_id=current_user["tenant_id"])
return ExampleResponse.model_validate(example)
# 3. GET endpoint - Retrieve single resource
@router.get("/{example_id}", response_model=ExampleResponse)
async def get_example(
example_id: UUID,
repo: Annotated[ExampleRepository, Depends(get_example_repository)],
current_user: Annotated[dict, Depends(get_current_user)],
) -> ExampleResponse:
"""
Get example by ID with tenant isolation.
Args:
example_id: Example UUID
repo: Example repository dependency
current_user: Currently authenticated user
Returns:
ExampleResponse: Example data
Raises:
HTTPException: If not found
"""
# Get with tenant isolation
example = await repo.get_by_id(example_id, tenant_id=current_user["tenant_id"])
if not example:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Example not found"
)
return ExampleResponse.model_validate(example)
# 4. GET endpoint - List resources with pagination
@router.get("/", response_model=list[ExampleResponse])
async def list_examples(
repo: Annotated[ExampleRepository, Depends(get_example_repository)],
current_user: Annotated[dict, Depends(get_current_user)],
limit: int = Query(100, ge=1, le=1000, description="Max items to return"),
offset: int = Query(0, ge=0, description="Number of items to skip"),
is_active: bool | None = Query(None, description="Filter by active status"),
) -> list[ExampleResponse]:
"""
List examples with pagination and filtering.
Args:
repo: Example repository dependency
current_user: Currently authenticated user
limit: Maximum number of items to return
offset: Number of items to skip
is_active: Optional filter by active status
Returns:
list[ExampleResponse]: List of examples
"""
# List with tenant isolation
examples = await repo.list_by_tenant(
tenant_id=current_user["tenant_id"],
limit=limit,
offset=offset,
is_active=is_active,
)
return [ExampleResponse.model_validate(e) for e in examples]
# 5. PATCH endpoint - Update resource
@router.patch("/{example_id}", response_model=ExampleResponse)
async def update_example(
example_id: UUID,
data: ExampleUpdate,
repo: Annotated[ExampleRepository, Depends(get_example_repository)],
current_user: Annotated[dict, Depends(get_current_user)],
) -> ExampleResponse:
"""
Update example by ID with tenant isolation.
Args:
example_id: Example UUID
data: Update data (partial fields)
repo: Example repository dependency
current_user: Currently authenticated user
Returns:
ExampleResponse: Updated example
Raises:
HTTPException: If not found
"""
# Get existing example with tenant isolation
example = await repo.get_by_id(example_id, tenant_id=current_user["tenant_id"])
if not example:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Example not found"
)
# Update fields (exclude_unset to only update provided fields)
update_dict = data.model_dump(exclude_unset=True)
for field, value in update_dict.items():
setattr(example, field, value)
# Save updates
updated = await repo.update(example)
return ExampleResponse.model_validate(updated)
# 6. DELETE endpoint - Delete resource
@router.delete("/{example_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_example(
example_id: UUID,
repo: Annotated[ExampleRepository, Depends(get_example_repository)],
current_user: Annotated[dict, Depends(get_current_user)],
) -> None:
"""
Delete example by ID with tenant isolation.
Args:
example_id: Example UUID
repo: Example repository dependency
current_user: Currently authenticated user
Raises:
HTTPException: If not found
"""
# Get existing example with tenant isolation
example = await repo.get_by_id(example_id, tenant_id=current_user["tenant_id"])
if not example:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Example not found"
)
# Delete
await repo.delete(example_id, tenant_id=current_user["tenant_id"])

View File

@@ -0,0 +1,126 @@
"""Example SQLModel Database Model Template.
Copy and adapt this for new Grey Haven database models.
"""
from __future__ import annotations
from datetime import datetime
from uuid import UUID, uuid4
from sqlalchemy import Column as SAColumn
from sqlmodel import JSON, Column, Field, SQLModel
def utc_now() -> datetime:
"""Return current UTC datetime."""
from datetime import UTC
return datetime.now(UTC)
class ExampleDB(SQLModel, table=True): # type: ignore[call-arg]
"""
Example database model with multi-tenant support.
This model demonstrates Grey Haven's database conventions:
- snake_case field names
- Multi-tenant isolation with tenant_id
- UTC timestamps
- Proper indexes
- Comprehensive docstrings
"""
__tablename__ = "examples"
# Primary identification
id: UUID = Field(
default_factory=uuid4, primary_key=True, description="Unique example identifier"
)
# Multi-tenant field (CRITICAL - always include!)
tenant_id: UUID = Field(
foreign_key="tenants.id", index=True, description="Owning tenant identifier"
)
# Example fields - all snake_case!
name: str = Field(index=True, max_length=255, description="Example name")
description: str | None = Field(
default=None, max_length=1000, description="Optional description"
)
# Relationships (foreign keys)
owner_id: UUID | None = Field(
default=None, foreign_key="users.id", index=True, description="Owner user ID"
)
# Status flags
is_active: bool = Field(default=True, description="Whether example is active")
is_archived: bool = Field(default=False, description="Whether example is archived")
# JSON metadata field
metadata: dict | None = Field(
default=None,
sa_column=Column(JSON),
description="Flexible JSON metadata storage",
)
# Numerical fields
priority: int = Field(
default=0, ge=0, le=10, description="Priority level (0-10)"
)
max_retries: int = Field(
default=3, ge=0, description="Maximum number of retry attempts"
)
# Timestamps (UTC)
created_at: datetime = Field(
default_factory=utc_now, description="Creation timestamp (UTC)"
)
updated_at: datetime = Field(
default_factory=utc_now, description="Last update timestamp (UTC)"
)
archived_at: datetime | None = Field(
default=None, description="Archive timestamp (UTC)"
)
# Uncomment if using custom UTCDateTime type
# from app.db.db_types import UTCDateTime
# created_at: datetime = Field(
# default_factory=utc_now,
# sa_column=SAColumn(UTCDateTime, nullable=False)
# )
# updated_at: datetime = Field(
# default_factory=utc_now,
# sa_column=SAColumn(UTCDateTime, nullable=False, onupdate=utc_now)
# )
# Pydantic schemas for API (in separate schemas file)
# class ExampleBase(BaseModel):
# """Base example schema with shared fields."""
# name: str = Field(..., max_length=255)
# description: str | None = None
# is_active: bool = True
# priority: int = Field(default=0, ge=0, le=10)
#
# class ExampleCreate(ExampleBase):
# """Schema for creating an example."""
# tenant_id: UUID
#
# class ExampleUpdate(BaseModel):
# """Schema for updating an example (all fields optional)."""
# name: str | None = None
# description: str | None = None
# is_active: bool | None = None
# priority: int | None = Field(None, ge=0, le=10)
#
# class ExampleResponse(ExampleBase):
# """Example response schema."""
# id: UUID
# tenant_id: UUID
# owner_id: UUID | None
# created_at: datetime
# updated_at: datetime
#
# model_config = ConfigDict(from_attributes=True)

View File

@@ -0,0 +1,73 @@
# Grey Haven Studio - Ruff Configuration Template
# Copy this to your project root as pyproject.toml or ruff.toml
[tool.ruff]
# CRITICAL: Line length is 130, not 80 or 88!
line-length = 130
indent-width = 4
# Auto-fix issues without showing unfixable errors
fix-only = true
show-fixes = true
# Python version
target-version = "py312"
[tool.ruff.lint]
# Enable specific linter rules
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort (import sorting)
"B", # flake8-bugbear (bug detection)
"C4", # flake8-comprehensions
"UP", # pyupgrade (automatic upgrades)
]
# Ignore specific rules if needed
ignore = []
[tool.ruff.format]
# Use double quotes for strings
quote-style = "double"
# Use spaces for indentation
indent-style = "space"
# Use Unix-style line endings
line-ending = "lf"
[tool.ruff.lint.isort]
# Configure import sorting
known-first-party = ["app"]
section-order = [
"future",
"standard-library",
"third-party",
"first-party",
"local-folder",
]
# MyPy Configuration (add to pyproject.toml)
# [tool.mypy]
# python_version = "3.12"
# warn_return_any = true
# warn_unused_configs = true
# disallow_untyped_defs = true # REQUIRED: Type hints on all functions!
# check_untyped_defs = true
# ignore_missing_imports = false
# strict_optional = true
# warn_redundant_casts = true
# warn_unused_ignores = true
# Pytest Configuration (add to pyproject.toml)
# [tool.pytest.ini_options]
# pythonpath = ["."]
# asyncio_mode = "auto"
# testpaths = ["tests"]
# markers = [
# "unit: Unit tests",
# "integration: Integration tests",
# "e2e: End-to-end tests",
# ]

View File

@@ -0,0 +1,85 @@
// Example React Component Template
// Copy and adapt this for new Grey Haven components
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { Button } from "~/lib/components/ui/button";
import { Card } from "~/lib/components/ui/card";
import { queryClient } from "~/lib/query-client";
// 1. Define your types/interfaces
interface MyComponentProps {
id: string;
onUpdate?: (data: MyData) => void;
}
interface MyData {
id: string;
name: string;
created_at: Date; // snake_case for database fields
is_active: boolean;
}
// 2. Component (default export for routes)
export default function MyComponent({ id, onUpdate }: MyComponentProps) {
// 3. State management
const [isEditing, setIsEditing] = useState(false);
// 4. Queries with TanStack Query
const { data, isLoading, error } = useQuery(
{
queryKey: ["myData", id],
queryFn: async () => {
// Replace with your API call
const response = await fetch(`/api/data/${id}`);
return response.json();
},
staleTime: 60000, // 1 minute - Grey Haven default
},
queryClient,
);
// 5. Event handlers
const handleSave = async () => {
// Replace with your save logic
console.log("Saving...");
setIsEditing(false);
onUpdate?.(data);
};
// 6. Conditional renders
if (isLoading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error loading data</div>;
}
if (!data) {
return <div>No data found</div>;
}
// 7. Main render
return (
<Card className="p-6">
<h2 className="mb-4 text-2xl font-bold">{data.name}</h2>
{isEditing ? (
<div className="space-y-4">
{/* Edit mode UI */}
<Button onClick={handleSave}>Save</Button>
<Button variant="outline" onClick={() => setIsEditing(false)}>
Cancel
</Button>
</div>
) : (
<div className="space-y-2">
{/* View mode UI */}
<p>Status: {data.is_active ? "Active" : "Inactive"}</p>
<Button onClick={() => setIsEditing(true)}>Edit</Button>
</div>
)}
</Card>
);
}

View File

@@ -0,0 +1,92 @@
// Example TanStack Start Server Function Template
// Copy and adapt this for new Grey Haven server functions
import { createServerFn } from "@tanstack/start";
import { db } from "~/lib/server/db";
import { users } from "~/lib/server/schema/users";
import { eq } from "drizzle-orm";
import { z } from "zod";
// 1. Define input schema with Zod
const getUserInputSchema = z.object({
userId: z.string().uuid(),
tenantId: z.string(), // Always include tenant_id for multi-tenant isolation
});
// 2. Define output type
interface UserOutput {
id: string;
name: string;
email: string;
created_at: Date;
tenant_id: string;
}
// 3. Create server function
export const getUser = createServerFn("GET", async (input: unknown): Promise<UserOutput> => {
// Validate input
const { userId, tenantId } = getUserInputSchema.parse(input);
// Query database with tenant isolation
const user = await db.query.users.findFirst({
where: eq(users.id, userId) && eq(users.tenant_id, tenantId), // Tenant filtering!
});
if (!user) {
throw new Error("User not found");
}
// Return typed result
return {
id: user.id,
name: user.name,
email: user.email,
created_at: user.created_at,
tenant_id: user.tenant_id,
};
});
// 4. Mutation example
const updateUserInputSchema = z.object({
userId: z.string().uuid(),
tenantId: z.string(),
name: z.string().min(1),
email: z.string().email(),
});
export const updateUser = createServerFn(
"POST",
async (input: unknown): Promise<UserOutput> => {
// Validate input
const { userId, tenantId, name, email } = updateUserInputSchema.parse(input);
// Update with tenant isolation
const [updatedUser] = await db
.update(users)
.set({ name, email, updated_at: new Date() })
.where(eq(users.id, userId) && eq(users.tenant_id, tenantId)) // Tenant filtering!
.returning();
if (!updatedUser) {
throw new Error("User not found or update failed");
}
return {
id: updatedUser.id,
name: updatedUser.name,
email: updatedUser.email,
created_at: updatedUser.created_at,
tenant_id: updatedUser.tenant_id,
};
},
);
// 5. Usage in client components
// import { useQuery } from "@tanstack/react-query";
// import { getUser } from "~/lib/server/functions/users";
//
// const { data: user } = useQuery({
// queryKey: ["user", userId],
// queryFn: () => getUser({ userId, tenantId }),
// staleTime: 60000,
// });