forked from retoor/devplacepy
update
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# Database Service (stub)
|
||||
@@ -0,0 +1,52 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from devplacepy.config import DATA_DIR, DATABASE_URL
|
||||
from devplacepy_services.base.sqlite_broker import SQLiteFileBroker
|
||||
|
||||
|
||||
def _db_path() -> Path:
|
||||
# DEVPLACE_DATABASE_URL is the documented override (tests use a dedicated
|
||||
# tempfile for isolation - see tests/conftest.py). DATABASE_URL already
|
||||
# falls back to DATA_DIR / "devplace.db" when the env var is unset, so
|
||||
# deriving the broker's path from it (instead of hardcoding DATA_DIR
|
||||
# directly) keeps the broker and every other DEVPLACE_DATABASE_URL
|
||||
# consumer (devplacepy/database/core.py) pointed at the same file.
|
||||
if DATABASE_URL.startswith("sqlite:///"):
|
||||
raw = DATABASE_URL[len("sqlite:///") :]
|
||||
if raw and raw != ":memory:":
|
||||
return Path(raw)
|
||||
return DATA_DIR / "devplace.db"
|
||||
|
||||
|
||||
_broker = SQLiteFileBroker("main", _db_path())
|
||||
|
||||
|
||||
def get_broker() -> SQLiteFileBroker:
|
||||
return _broker
|
||||
|
||||
|
||||
async def startup() -> None:
|
||||
await _broker.startup()
|
||||
from devplacepy_services.database.db_patch import patch_all_db, set_fallback_db
|
||||
|
||||
patch_all_db()
|
||||
set_fallback_db(_broker._write_db)
|
||||
os.environ["DEVPLACE_DB_SERVICE"] = "1"
|
||||
from devplacepy.database import init_db
|
||||
|
||||
init_db()
|
||||
from devplacepy.database.settings import internal_gateway_key
|
||||
|
||||
key = (internal_gateway_key() or "").strip()
|
||||
if key:
|
||||
os.environ["DEVPLACE_GATEWAY_INTERNAL_KEY"] = key
|
||||
(DATA_DIR / ".internal_key").write_text(key + "\n")
|
||||
|
||||
|
||||
async def shutdown() -> None:
|
||||
await _broker.shutdown()
|
||||
@@ -0,0 +1,135 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from devplacepy.database import (
|
||||
get_daily_topic,
|
||||
get_polls_by_post_uids,
|
||||
get_reactions_by_targets,
|
||||
get_recent_comments_by_post_uids,
|
||||
get_site_stats,
|
||||
get_top_authors,
|
||||
get_user_bookmarks,
|
||||
get_users_by_uids,
|
||||
)
|
||||
from devplacepy.database.attachments_data import get_attachments_by_type
|
||||
from devplacepy.routers.feed import get_feed_posts
|
||||
|
||||
|
||||
def build_feed_page(
|
||||
user: dict | None = None,
|
||||
tab: str = "all",
|
||||
topic: str | None = None,
|
||||
search: str = "",
|
||||
before: str | None = None,
|
||||
) -> dict:
|
||||
posts, next_cursor = get_feed_posts(user, tab, topic, search, before)
|
||||
stats = get_site_stats()
|
||||
top_authors = get_top_authors(5)
|
||||
daily_topic = get_daily_topic()
|
||||
post_uids = [item["post"]["uid"] for item in posts]
|
||||
attachments_map = get_attachments_by_type("post", post_uids)
|
||||
recent_comments = get_recent_comments_by_post_uids(post_uids, 3, user)
|
||||
reactions_map = get_reactions_by_targets("post", post_uids, user)
|
||||
bookmark_set = (
|
||||
get_user_bookmarks(user["uid"], "post", post_uids) if user else set()
|
||||
)
|
||||
polls_map = get_polls_by_post_uids(post_uids, user)
|
||||
for item in posts:
|
||||
uid = item["post"]["uid"]
|
||||
item["attachments"] = attachments_map.get(uid, [])
|
||||
item["recent_comments"] = recent_comments.get(uid, [])
|
||||
item["reactions"] = reactions_map.get(uid, {"counts": {}, "mine": []})
|
||||
item["bookmarked"] = uid in bookmark_set
|
||||
item["poll"] = polls_map.get(uid)
|
||||
return {
|
||||
"posts": posts,
|
||||
"next_cursor": next_cursor,
|
||||
"stats": stats,
|
||||
"top_authors": top_authors,
|
||||
"daily_topic": daily_topic,
|
||||
"online_users": [],
|
||||
"current_tab": tab,
|
||||
"current_topic": topic,
|
||||
"search": search,
|
||||
}
|
||||
|
||||
|
||||
def build_post_detail(post_uid: str, user: dict | None = None) -> dict:
|
||||
from devplacepy.database import get_table, load_comments
|
||||
|
||||
post = get_table("posts").find_one(uid=post_uid, deleted_at=None)
|
||||
if not post:
|
||||
return {"post": None, "author": None, "comments": [], "attachments": []}
|
||||
author = get_users_by_uids([post["user_uid"]]).get(post["user_uid"])
|
||||
comments = load_comments("post", post_uid, user)
|
||||
attachments = get_attachments_by_type("post", [post_uid]).get(post_uid, [])
|
||||
return {
|
||||
"post": post,
|
||||
"author": author,
|
||||
"comments": comments,
|
||||
"attachments": attachments,
|
||||
}
|
||||
|
||||
|
||||
def build_profile_bundle(profile_uid: str, viewer: dict | None = None) -> dict:
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.database.follows import get_follow_counts
|
||||
|
||||
user = get_table("users").find_one(uid=profile_uid)
|
||||
if not user:
|
||||
return {"user": None, "follow_counts": {}, "awards": [], "posts_count": 0}
|
||||
follow_counts = get_follow_counts(profile_uid)
|
||||
return {
|
||||
"user": user,
|
||||
"follow_counts": follow_counts,
|
||||
"awards": [],
|
||||
"posts_count": 0,
|
||||
"viewer_relation": {},
|
||||
}
|
||||
|
||||
|
||||
def build_messages_page(user_uid: str) -> dict:
|
||||
return {"threads": [], "unread": 0, "partners": {}}
|
||||
|
||||
|
||||
def build_notifications_page(user_uid: str) -> dict:
|
||||
return {"notifications": [], "actors": {}, "unread_count": 0}
|
||||
|
||||
|
||||
def build_leaderboard_page(viewer_uid: str | None = None) -> dict:
|
||||
from devplacepy.database import get_leaderboard, get_user_rank
|
||||
|
||||
return {
|
||||
"leaderboard": get_leaderboard(),
|
||||
"viewer_rank": get_user_rank(viewer_uid) if viewer_uid else None,
|
||||
}
|
||||
|
||||
|
||||
def build_project_detail(project_uid: str) -> dict:
|
||||
from devplacepy.database import get_table
|
||||
|
||||
project = get_table("projects").find_one(uid=project_uid, deleted_at=None)
|
||||
owner = None
|
||||
if project:
|
||||
owner = get_users_by_uids([project["user_uid"]]).get(project["user_uid"])
|
||||
return {"project": project, "owner": owner, "files": [], "containers": []}
|
||||
|
||||
|
||||
def build_gist_detail(gist_uid: str, user: dict | None = None) -> dict:
|
||||
from devplacepy.database import get_table, load_comments
|
||||
|
||||
gist = get_table("gists").find_one(uid=gist_uid, deleted_at=None)
|
||||
author = None
|
||||
if gist:
|
||||
author = get_users_by_uids([gist["user_uid"]]).get(gist["user_uid"])
|
||||
comments = load_comments("gist", gist_uid, user) if gist else []
|
||||
return {"gist": gist, "author": author, "comments": comments}
|
||||
|
||||
|
||||
def build_game_state(user_uid: str | None = None) -> dict:
|
||||
return {"store": {}, "leaderboard": []}
|
||||
|
||||
|
||||
def build_admin_dashboard() -> dict:
|
||||
return {"stats": get_site_stats(), "service_states": []}
|
||||
@@ -0,0 +1,138 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from devplacepy.database import (
|
||||
get_attachments_by_type,
|
||||
get_comment_counts_by_post_uids,
|
||||
get_follow_counts,
|
||||
get_following_among,
|
||||
get_leaderboard,
|
||||
get_muted_uids,
|
||||
get_blocked_uids,
|
||||
get_user_relations,
|
||||
get_notification_prefs,
|
||||
get_online_users,
|
||||
get_polls_by_post_uids,
|
||||
get_prominent_award,
|
||||
get_reactions_by_targets,
|
||||
get_recent_comments_by_post_uids,
|
||||
get_seo_metadata_batch,
|
||||
get_site_stats,
|
||||
get_top_authors,
|
||||
get_user_awards,
|
||||
get_user_bookmarks,
|
||||
get_user_media,
|
||||
get_user_rank,
|
||||
get_users_by_uids,
|
||||
get_vote_counts,
|
||||
load_comments,
|
||||
)
|
||||
|
||||
|
||||
def users_by_uids(uids: list[str]) -> dict:
|
||||
return get_users_by_uids(uids)
|
||||
|
||||
|
||||
def comment_counts(post_uids: list[str]) -> dict:
|
||||
return get_comment_counts_by_post_uids(post_uids)
|
||||
|
||||
|
||||
def vote_counts(target_uids: list[str]) -> dict:
|
||||
ups, downs = get_vote_counts(target_uids)
|
||||
return {"ups": ups, "downs": downs}
|
||||
|
||||
|
||||
def reactions(target_type: str, target_uids: list[str], user: dict | None = None) -> dict:
|
||||
return get_reactions_by_targets(target_type, target_uids, user)
|
||||
|
||||
|
||||
def bookmarks(user_uid: str, target_type: str, target_uids: list[str]) -> list[str]:
|
||||
result = get_user_bookmarks(user_uid, target_type, target_uids)
|
||||
return sorted(result)
|
||||
|
||||
|
||||
def polls(post_uids: list[str], user: dict | None = None) -> dict:
|
||||
return get_polls_by_post_uids(post_uids, user)
|
||||
|
||||
|
||||
def recent_comments(
|
||||
post_uids: list[str], limit: int = 3, user: dict | None = None
|
||||
) -> dict:
|
||||
return get_recent_comments_by_post_uids(post_uids, limit, user)
|
||||
|
||||
|
||||
def comments(target_type: str, target_uid: str, user: dict | None = None) -> list:
|
||||
return load_comments(target_type, target_uid, user)
|
||||
|
||||
|
||||
def follow_bundle(user_uid: str, target_uids: list[str] | None = None) -> dict:
|
||||
payload = {
|
||||
"counts": get_follow_counts(user_uid),
|
||||
"following_among": sorted(get_following_among(user_uid, target_uids or [])),
|
||||
}
|
||||
return payload
|
||||
|
||||
|
||||
def relations_bundle(viewer_uid: str | None) -> dict:
|
||||
relations = get_user_relations(viewer_uid)
|
||||
return {
|
||||
"relations": {
|
||||
"block": sorted(relations["block"]),
|
||||
"mute": sorted(relations["mute"]),
|
||||
},
|
||||
"blocked_uids": sorted(get_blocked_uids(viewer_uid)),
|
||||
"muted_uids": sorted(get_muted_uids(viewer_uid)),
|
||||
}
|
||||
|
||||
|
||||
def online_users_bundle(cutoff_iso: str, limit: int = 30) -> list:
|
||||
return get_online_users(cutoff_iso, limit)
|
||||
|
||||
|
||||
def notification_prefs_bundle(user_uid: str) -> list:
|
||||
return get_notification_prefs(user_uid)
|
||||
|
||||
|
||||
def leaderboard_bundle(
|
||||
limit: int = 50, offset: int = 0, viewer_uid: str | None = None
|
||||
) -> dict:
|
||||
return {
|
||||
"leaderboard": get_leaderboard(limit, offset),
|
||||
"viewer_rank": get_user_rank(viewer_uid) if viewer_uid else None,
|
||||
}
|
||||
|
||||
|
||||
def site_sidebar(authors_limit: int = 5) -> dict:
|
||||
return {
|
||||
"stats": get_site_stats(),
|
||||
"top_authors": get_top_authors(authors_limit),
|
||||
}
|
||||
|
||||
|
||||
def awards_bundle(
|
||||
receiver_uid: str,
|
||||
page: int = 1,
|
||||
per_page: int = 12,
|
||||
profile_user: dict | None = None,
|
||||
) -> dict:
|
||||
items, pagination = get_user_awards(receiver_uid, page, per_page)
|
||||
prominent = get_prominent_award(profile_user) if profile_user else None
|
||||
return {
|
||||
"items": items,
|
||||
"pagination": pagination,
|
||||
"prominent": prominent,
|
||||
}
|
||||
|
||||
|
||||
def attachments_batch(resource_type: str, resource_uids: list[str]) -> dict:
|
||||
return get_attachments_by_type(resource_type, resource_uids)
|
||||
|
||||
|
||||
def user_media_bundle(user_uid: str, page: int = 1, per_page: int = 24) -> dict:
|
||||
items, pagination = get_user_media(user_uid, page, per_page)
|
||||
return {"items": items, "pagination": pagination}
|
||||
|
||||
|
||||
def seo_meta_batch(target_type: str, uids: list[str]) -> dict:
|
||||
return get_seo_metadata_batch(target_type, uids)
|
||||
@@ -0,0 +1,119 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextvars
|
||||
import importlib
|
||||
from typing import Any
|
||||
|
||||
_DB_MODULES = (
|
||||
"schema",
|
||||
"content",
|
||||
"pagination",
|
||||
"stats",
|
||||
"forks",
|
||||
"awards",
|
||||
"customization",
|
||||
"soft_delete",
|
||||
"usage",
|
||||
"email",
|
||||
"follows",
|
||||
"settings",
|
||||
"seo_meta",
|
||||
"deepsearch",
|
||||
"activity",
|
||||
"attachments_data",
|
||||
"users",
|
||||
"notifications",
|
||||
"comments",
|
||||
"ranking",
|
||||
"engagement",
|
||||
"relations",
|
||||
)
|
||||
|
||||
_EXTERNAL_DB_MODULES = (
|
||||
"devplacepy.services.audit.store",
|
||||
"devplacepy.services.backup.store",
|
||||
"devplacepy.services.openai_gateway.routing",
|
||||
"devplacepy.services.openai_gateway.usage",
|
||||
"devplacepy.services.openai_gateway.analytics",
|
||||
"devplacepy.services.base",
|
||||
"devplacepy.services.jobs.queue",
|
||||
"devplacepy.services.containers.store",
|
||||
"devplacepy.services.devii.store",
|
||||
"devplacepy.services.devii.hub",
|
||||
"devplacepy.attachments",
|
||||
"devplacepy.project_files",
|
||||
)
|
||||
|
||||
# The read pool runs on asyncio.to_thread worker threads while the write
|
||||
# lane runs on the main event loop task - a plain module-global "current
|
||||
# connection" swapped in/out per call is a data race between them (a
|
||||
# concurrent read could reassign the global to a read-only connection
|
||||
# while a write is still mid-flight, raising "attempt to write a readonly
|
||||
# database"). asyncio.to_thread propagates a COPY of the calling task's
|
||||
# contextvars.Context into the new thread, so storing the active
|
||||
# connection in a ContextVar gives each call its own isolated view with
|
||||
# no cross-talk, while every db-layer module keeps a single shared proxy
|
||||
# object as its permanent `db` binding.
|
||||
_CURRENT_DB: contextvars.ContextVar[Any] = contextvars.ContextVar("current_db", default=None)
|
||||
|
||||
# Fallback for any code path that touches the db outside an explicit
|
||||
# use_db()-scoped call (e.g. startup's own init_db()) - write-once at
|
||||
# broker startup, read-only afterward, so it carries no race of its own.
|
||||
_fallback_db: Any = None
|
||||
|
||||
|
||||
def _resolve() -> Any:
|
||||
db = _CURRENT_DB.get()
|
||||
if db is not None:
|
||||
return db
|
||||
if _fallback_db is not None:
|
||||
return _fallback_db
|
||||
raise RuntimeError("no database connection configured for this context")
|
||||
|
||||
|
||||
class _ContextDb:
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
return getattr(_resolve(), name)
|
||||
|
||||
def __getitem__(self, name: str) -> Any:
|
||||
return _resolve()[name]
|
||||
|
||||
def __enter__(self):
|
||||
return _resolve().__enter__()
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
return _resolve().__exit__(exc_type, exc, tb)
|
||||
|
||||
|
||||
_CONTEXT_DB = _ContextDb()
|
||||
|
||||
|
||||
def patch_all_db() -> None:
|
||||
import devplacepy.database as database_pkg
|
||||
import devplacepy.database.core as core
|
||||
|
||||
core.db = _CONTEXT_DB
|
||||
database_pkg.db = _CONTEXT_DB
|
||||
for name in _DB_MODULES:
|
||||
mod = importlib.import_module(f"devplacepy.database.{name}")
|
||||
if hasattr(mod, "db"):
|
||||
mod.db = _CONTEXT_DB
|
||||
for name in _EXTERNAL_DB_MODULES:
|
||||
mod = importlib.import_module(name)
|
||||
if hasattr(mod, "db"):
|
||||
mod.db = _CONTEXT_DB
|
||||
|
||||
|
||||
def set_fallback_db(db_conn) -> None:
|
||||
global _fallback_db
|
||||
_fallback_db = db_conn
|
||||
|
||||
|
||||
def use_db(db_conn) -> contextvars.Token:
|
||||
return _CURRENT_DB.set(db_conn)
|
||||
|
||||
|
||||
def reset_db(token: contextvars.Token) -> None:
|
||||
_CURRENT_DB.reset(token)
|
||||
@@ -0,0 +1,40 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
from typing import Any
|
||||
|
||||
import devplacepy.database as db_module
|
||||
|
||||
from devplacepy_services.base.db_codec import decode_arg, encode_result, is_write
|
||||
|
||||
|
||||
def build_registry() -> dict[str, Any]:
|
||||
registry: dict[str, Any] = {}
|
||||
for name in db_module.__all__:
|
||||
target = getattr(db_module, name, None)
|
||||
if target is None or not callable(target):
|
||||
continue
|
||||
if inspect.isclass(target):
|
||||
continue
|
||||
registry[name] = target
|
||||
return registry
|
||||
|
||||
|
||||
REGISTRY = build_registry()
|
||||
|
||||
|
||||
_encode_result = encode_result
|
||||
|
||||
|
||||
def run_with_db(db_conn, fn, args, kwargs):
|
||||
from devplacepy_services.database.db_patch import use_db, reset_db
|
||||
|
||||
token = use_db(db_conn)
|
||||
try:
|
||||
decoded_args = decode_arg(args)
|
||||
decoded_kwargs = decode_arg(kwargs)
|
||||
return fn(*decoded_args, **decoded_kwargs)
|
||||
finally:
|
||||
reset_db(token)
|
||||
@@ -0,0 +1,52 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
from devplacepy_services.base.health import health_router
|
||||
from devplacepy_services.base.http import shutdown_pool, startup_pool
|
||||
from devplacepy_services.base.middleware import (
|
||||
InternalAuthMiddleware,
|
||||
InternalCallCounterMiddleware,
|
||||
RequestStatsMiddleware,
|
||||
SanitizeErrorsMiddleware,
|
||||
)
|
||||
from devplacepy_services.base.service import BaseMicroservice
|
||||
from devplacepy_services.database import broker_setup
|
||||
from devplacepy_services.database.routes import router as database_router
|
||||
|
||||
|
||||
class DatabaseService(BaseMicroservice):
|
||||
name = "database"
|
||||
title = "Database"
|
||||
default_port = 10601
|
||||
workers = 1
|
||||
stateful = True
|
||||
depends_on = []
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(self, app: FastAPI):
|
||||
app.state.service_name = self.name
|
||||
self.started_at = __import__("time").monotonic()
|
||||
await startup_pool()
|
||||
await broker_setup.startup()
|
||||
yield
|
||||
await broker_setup.shutdown()
|
||||
await shutdown_pool()
|
||||
|
||||
def build_app(self):
|
||||
app = self.create_app()
|
||||
app.add_middleware(SanitizeErrorsMiddleware)
|
||||
app.add_middleware(InternalAuthMiddleware)
|
||||
app.add_middleware(InternalCallCounterMiddleware)
|
||||
app.add_middleware(RequestStatsMiddleware)
|
||||
app.include_router(health_router(self))
|
||||
app.include_router(database_router)
|
||||
return app
|
||||
|
||||
|
||||
_service = DatabaseService()
|
||||
app = _service.build_app()
|
||||
@@ -0,0 +1,421 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from devplacepy.database import bump_cache_version, get_int_setting, get_setting
|
||||
from devplacepy_services.base.auth import internal_user
|
||||
from devplacepy_services.base.compound import (
|
||||
AttachmentsIn,
|
||||
AwardsBundleIn,
|
||||
BookmarksIn,
|
||||
CacheBumpIn,
|
||||
CommentCountsIn,
|
||||
CommentsIn,
|
||||
FeedPageIn,
|
||||
FollowBundleIn,
|
||||
GameStateIn,
|
||||
GistDetailIn,
|
||||
InvokeIn,
|
||||
LeaderboardBundleIn,
|
||||
LeaderboardPageIn,
|
||||
MessagesPageIn,
|
||||
NotificationPrefsIn,
|
||||
NotificationsPageIn,
|
||||
OnlineUsersIn,
|
||||
PollsIn,
|
||||
PostDetailIn,
|
||||
ProfileBundleIn,
|
||||
ProjectDetailIn,
|
||||
ReactionsIn,
|
||||
RecentCommentsIn,
|
||||
RelationsIn,
|
||||
SeoMetaIn,
|
||||
SiteSidebarIn,
|
||||
UserMediaIn,
|
||||
UsersByUidsIn,
|
||||
VoteCountsIn,
|
||||
)
|
||||
from devplacepy_services.base.db_codec import is_write_sql
|
||||
from devplacepy_services.base.schemas import InternalUser
|
||||
from devplacepy_services.database import compounds_page, compounds_primitive
|
||||
from devplacepy_services.database.broker_setup import get_broker
|
||||
from devplacepy_services.database.invoke_registry import (
|
||||
REGISTRY,
|
||||
_encode_result,
|
||||
is_write,
|
||||
run_with_db,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class DbOpIn(BaseModel):
|
||||
op: str
|
||||
table: str | None = None
|
||||
method: str | None = None
|
||||
args: list[Any] = Field(default_factory=list)
|
||||
kwargs: dict[str, Any] = Field(default_factory=dict)
|
||||
write: bool = False
|
||||
|
||||
|
||||
def _db_tables(db_conn):
|
||||
return list(db_conn.tables)
|
||||
|
||||
|
||||
def _db_query(db_conn, sql: str, **params):
|
||||
return list(db_conn.query(sql, **params))
|
||||
|
||||
|
||||
def _db_table_op(db_conn, table: str, method: str, args: list, kwargs: dict):
|
||||
target = db_conn[table]
|
||||
fn = getattr(target, method)
|
||||
result = fn(*args, **kwargs)
|
||||
if hasattr(result, "__iter__") and not isinstance(result, (str, bytes, dict)):
|
||||
try:
|
||||
return [_encode_result(row) for row in result]
|
||||
except TypeError:
|
||||
pass
|
||||
return _encode_result(result)
|
||||
|
||||
|
||||
@router.post("/internal/invoke")
|
||||
async def internal_invoke(body: InvokeIn, _: InternalUser = Depends(internal_user)):
|
||||
fn = REGISTRY.get(body.fn)
|
||||
if fn is None:
|
||||
return {"error": f"Unknown function: {body.fn}", "code": "unknown_function"}
|
||||
write = body.write or is_write(body.fn)
|
||||
broker = get_broker()
|
||||
if write:
|
||||
result = await broker.write(run_with_db, fn, body.args, body.kwargs)
|
||||
else:
|
||||
result = await broker.read(run_with_db, fn, body.args, body.kwargs)
|
||||
return {"result": _encode_result(result)}
|
||||
|
||||
|
||||
@router.post("/internal/db-op")
|
||||
async def internal_db_op(body: DbOpIn, _: InternalUser = Depends(internal_user)):
|
||||
broker = get_broker()
|
||||
if body.op == "tables":
|
||||
tables = await broker.read(_db_tables)
|
||||
return tables
|
||||
if body.op == "query":
|
||||
sql = body.args[0] if body.args else ""
|
||||
write = body.write or is_write_sql(sql)
|
||||
if write:
|
||||
rows = await broker.write(_db_query, sql, **body.kwargs)
|
||||
else:
|
||||
rows = await broker.read(_db_query, sql, **body.kwargs)
|
||||
return [_encode_result(row) for row in rows]
|
||||
if body.op == "table_op":
|
||||
if not body.table or not body.method:
|
||||
return {"error": "table and method required", "code": "invalid_request"}
|
||||
write_methods = {"insert", "update", "delete", "create_column_by_example"}
|
||||
write = body.write or body.method in write_methods
|
||||
if write:
|
||||
result = await broker.write(
|
||||
_db_table_op, body.table, body.method, body.args, body.kwargs
|
||||
)
|
||||
else:
|
||||
result = await broker.read(
|
||||
_db_table_op, body.table, body.method, body.args, body.kwargs
|
||||
)
|
||||
return result
|
||||
return {"error": f"Unknown op: {body.op}", "code": "unknown_op"}
|
||||
|
||||
|
||||
@router.get("/settings/{key}")
|
||||
async def settings_get(key: str, default: str = "", _: InternalUser = Depends(internal_user)):
|
||||
broker = get_broker()
|
||||
value = await broker.read(run_with_db, get_setting, [key, default], {})
|
||||
return {"key": key, "value": value}
|
||||
|
||||
|
||||
@router.post("/cache/bump")
|
||||
async def cache_bump(body: CacheBumpIn, _: InternalUser = Depends(internal_user)):
|
||||
broker = get_broker()
|
||||
await broker.write(run_with_db, bump_cache_version, [body.name], {})
|
||||
return {"ok": True, "name": body.name}
|
||||
|
||||
|
||||
@router.post("/compound/users-by-uids")
|
||||
async def compound_users_by_uids(body: UsersByUidsIn, _: InternalUser = Depends(internal_user)):
|
||||
broker = get_broker()
|
||||
return await broker.read(run_with_db, compounds_primitive.users_by_uids, [body.uids], {})
|
||||
|
||||
|
||||
@router.post("/compound/comment-counts")
|
||||
async def compound_comment_counts(body: CommentCountsIn, _: InternalUser = Depends(internal_user)):
|
||||
broker = get_broker()
|
||||
return await broker.read(
|
||||
run_with_db, compounds_primitive.comment_counts, [body.post_uids], {}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compound/vote-counts")
|
||||
async def compound_vote_counts(body: VoteCountsIn, _: InternalUser = Depends(internal_user)):
|
||||
broker = get_broker()
|
||||
return await broker.read(
|
||||
run_with_db, compounds_primitive.vote_counts, [body.target_uids], {}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compound/reactions")
|
||||
async def compound_reactions(body: ReactionsIn, _: InternalUser = Depends(internal_user)):
|
||||
broker = get_broker()
|
||||
return await broker.read(
|
||||
run_with_db,
|
||||
compounds_primitive.reactions,
|
||||
[body.target_type, body.target_uids, body.user],
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compound/bookmarks")
|
||||
async def compound_bookmarks(body: BookmarksIn, _: InternalUser = Depends(internal_user)):
|
||||
broker = get_broker()
|
||||
return await broker.read(
|
||||
run_with_db,
|
||||
compounds_primitive.bookmarks,
|
||||
[body.user_uid, body.target_type, body.target_uids],
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compound/polls")
|
||||
async def compound_polls(body: PollsIn, _: InternalUser = Depends(internal_user)):
|
||||
broker = get_broker()
|
||||
return await broker.read(
|
||||
run_with_db, compounds_primitive.polls, [body.post_uids, body.user], {}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compound/recent-comments")
|
||||
async def compound_recent_comments(
|
||||
body: RecentCommentsIn, _: InternalUser = Depends(internal_user)
|
||||
):
|
||||
broker = get_broker()
|
||||
return await broker.read(
|
||||
run_with_db,
|
||||
compounds_primitive.recent_comments,
|
||||
[body.post_uids, body.limit, body.user],
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compound/comments")
|
||||
async def compound_comments(body: CommentsIn, _: InternalUser = Depends(internal_user)):
|
||||
broker = get_broker()
|
||||
return await broker.read(
|
||||
run_with_db,
|
||||
compounds_primitive.comments,
|
||||
[body.target_type, body.target_uid, body.user],
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compound/follow-bundle")
|
||||
async def compound_follow_bundle(body: FollowBundleIn, _: InternalUser = Depends(internal_user)):
|
||||
broker = get_broker()
|
||||
return await broker.read(
|
||||
run_with_db,
|
||||
compounds_primitive.follow_bundle,
|
||||
[body.user_uid, body.target_uids],
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compound/relations")
|
||||
async def compound_relations(body: RelationsIn, _: InternalUser = Depends(internal_user)):
|
||||
broker = get_broker()
|
||||
return await broker.read(
|
||||
run_with_db, compounds_primitive.relations_bundle, [body.viewer_uid], {}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compound/online-users")
|
||||
async def compound_online_users(body: OnlineUsersIn, _: InternalUser = Depends(internal_user)):
|
||||
broker = get_broker()
|
||||
return await broker.read(
|
||||
run_with_db,
|
||||
compounds_primitive.online_users_bundle,
|
||||
[body.cutoff_iso, body.limit],
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compound/notification-prefs")
|
||||
async def compound_notification_prefs(
|
||||
body: NotificationPrefsIn, _: InternalUser = Depends(internal_user)
|
||||
):
|
||||
broker = get_broker()
|
||||
return await broker.read(
|
||||
run_with_db,
|
||||
compounds_primitive.notification_prefs_bundle,
|
||||
[body.user_uid],
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compound/leaderboard")
|
||||
async def compound_leaderboard(
|
||||
body: LeaderboardBundleIn, _: InternalUser = Depends(internal_user)
|
||||
):
|
||||
broker = get_broker()
|
||||
return await broker.read(
|
||||
run_with_db,
|
||||
compounds_primitive.leaderboard_bundle,
|
||||
[body.limit, body.offset, body.viewer_uid],
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compound/site-sidebar")
|
||||
async def compound_site_sidebar(body: SiteSidebarIn, _: InternalUser = Depends(internal_user)):
|
||||
broker = get_broker()
|
||||
return await broker.read(
|
||||
run_with_db, compounds_primitive.site_sidebar, [body.authors_limit], {}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compound/awards")
|
||||
async def compound_awards(body: AwardsBundleIn, _: InternalUser = Depends(internal_user)):
|
||||
broker = get_broker()
|
||||
return await broker.read(
|
||||
run_with_db,
|
||||
compounds_primitive.awards_bundle,
|
||||
[body.receiver_uid, body.page, body.per_page, body.profile_user],
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compound/attachments")
|
||||
async def compound_attachments(body: AttachmentsIn, _: InternalUser = Depends(internal_user)):
|
||||
broker = get_broker()
|
||||
return await broker.read(
|
||||
run_with_db,
|
||||
compounds_primitive.attachments_batch,
|
||||
[body.resource_type, body.resource_uids],
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compound/user-media")
|
||||
async def compound_user_media(body: UserMediaIn, _: InternalUser = Depends(internal_user)):
|
||||
broker = get_broker()
|
||||
return await broker.read(
|
||||
run_with_db,
|
||||
compounds_primitive.user_media_bundle,
|
||||
[body.user_uid, body.page, body.per_page],
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compound/seo-meta")
|
||||
async def compound_seo_meta(body: SeoMetaIn, _: InternalUser = Depends(internal_user)):
|
||||
broker = get_broker()
|
||||
return await broker.read(
|
||||
run_with_db,
|
||||
compounds_primitive.seo_meta_batch,
|
||||
[body.target_type, body.uids],
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compound/feed-page")
|
||||
async def compound_feed_page(body: FeedPageIn, _: InternalUser = Depends(internal_user)):
|
||||
broker = get_broker()
|
||||
return await broker.read(
|
||||
run_with_db, compounds_page.build_feed_page, [], body.model_dump()
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compound/post-detail")
|
||||
async def compound_post_detail(body: PostDetailIn, _: InternalUser = Depends(internal_user)):
|
||||
broker = get_broker()
|
||||
return await broker.read(
|
||||
run_with_db,
|
||||
compounds_page.build_post_detail,
|
||||
[body.post_uid, body.user],
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compound/profile-bundle")
|
||||
async def compound_profile_bundle(
|
||||
body: ProfileBundleIn, _: InternalUser = Depends(internal_user)
|
||||
):
|
||||
broker = get_broker()
|
||||
return await broker.read(
|
||||
run_with_db,
|
||||
compounds_page.build_profile_bundle,
|
||||
[body.profile_uid, body.viewer],
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compound/messages-page")
|
||||
async def compound_messages_page(body: MessagesPageIn, _: InternalUser = Depends(internal_user)):
|
||||
broker = get_broker()
|
||||
return await broker.read(
|
||||
run_with_db, compounds_page.build_messages_page, [body.user_uid], {}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compound/notifications-page")
|
||||
async def compound_notifications_page(
|
||||
body: NotificationsPageIn, _: InternalUser = Depends(internal_user)
|
||||
):
|
||||
broker = get_broker()
|
||||
return await broker.read(
|
||||
run_with_db, compounds_page.build_notifications_page, [body.user_uid], {}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compound/leaderboard-page")
|
||||
async def compound_leaderboard_page(
|
||||
body: LeaderboardPageIn, _: InternalUser = Depends(internal_user)
|
||||
):
|
||||
broker = get_broker()
|
||||
return await broker.read(
|
||||
run_with_db, compounds_page.build_leaderboard_page, [body.viewer_uid], {}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compound/project-detail")
|
||||
async def compound_project_detail(
|
||||
body: ProjectDetailIn, _: InternalUser = Depends(internal_user)
|
||||
):
|
||||
broker = get_broker()
|
||||
return await broker.read(
|
||||
run_with_db, compounds_page.build_project_detail, [body.project_uid], {}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compound/gist-detail")
|
||||
async def compound_gist_detail(body: GistDetailIn, _: InternalUser = Depends(internal_user)):
|
||||
broker = get_broker()
|
||||
return await broker.read(
|
||||
run_with_db,
|
||||
compounds_page.build_gist_detail,
|
||||
[body.gist_uid, body.user],
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compound/game-state")
|
||||
async def compound_game_state(body: GameStateIn, _: InternalUser = Depends(internal_user)):
|
||||
broker = get_broker()
|
||||
return await broker.read(
|
||||
run_with_db, compounds_page.build_game_state, [body.user_uid], {}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compound/admin-dashboard")
|
||||
async def compound_admin_dashboard(_: InternalUser = Depends(internal_user)):
|
||||
broker = get_broker()
|
||||
return await broker.read(run_with_db, compounds_page.build_admin_dashboard, [], {})
|
||||
Reference in New Issue
Block a user