Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,8 @@ cython_debug/
marimo/_static/
marimo/_lsp/
__marimo__/

# ignore dynamic version file
_version.py
# ignore VSCode launch configurations (debugging configurations which are user-specific)
.vscode/launch.json
22 changes: 0 additions & 22 deletions NOTICE

This file was deleted.

5 changes: 0 additions & 5 deletions backend/database.py

This file was deleted.

19 changes: 0 additions & 19 deletions backend/datamodel.py

This file was deleted.

109 changes: 0 additions & 109 deletions backend/main.py

This file was deleted.

File renamed without changes.
Empty file added jsoned/api/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions jsoned/api/v1/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from fastapi import APIRouter

api_router = APIRouter()
8 changes: 8 additions & 0 deletions jsoned/api/v1/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from fastapi import APIRouter

from jsoned.api.v1.endpoints import schemas

api_router = APIRouter()
api_router.include_router(schemas.router, prefix="/schemas", tags=["schemas"])
# TODO add login with oauth2
# api_router.include_router(login.router, prefix="/login", tags=["login"])
Empty file.
141 changes: 141 additions & 0 deletions jsoned/api/v1/endpoints/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
from datetime import datetime, timezone

from bson import ObjectId
from bson.errors import InvalidId
from fastapi import APIRouter, HTTPException, Request, status
from pymongo.errors import DuplicateKeyError

from jsoned.models.content import ContentUpdate, UpdateResponse, hash_content
from jsoned.models.schema_definition import SchemaDefinition

router = APIRouter()


@router.get("/all", response_model=list[SchemaDefinition])
async def get_all_schemas(request: Request):
collection = request.app.state.schemas_collection
docs = collection.find({})
all_docs = []
async for doc in docs:
doc["_id"] = str(doc["_id"])
all_docs.append(SchemaDefinition(**doc))
return all_docs


@router.post(
"/add", response_model=SchemaDefinition, status_code=status.HTTP_201_CREATED
)
async def add_schema(request: Request, schema: SchemaDefinition):
if schema.content is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="`content` must not be null.",
)

collection = request.app.state.schemas_collection

# Build doc to insert, excluding None fields
doc = schema.model_dump(exclude_none=True)
doc["created_at"] = datetime.now(timezone.utc)
doc["updated_at"] = doc["created_at"]
doc["content_hash"] = hash_content(schema.content)

try:
result = await collection.insert_one(doc)
except DuplicateKeyError:
# you created a unique index on title
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="A schema with this title already exists.",
)
doc["_id"] = str(result.inserted_id)
return SchemaDefinition(**doc)


@router.delete("/delete/{title}", status_code=status.HTTP_200_OK)
async def delete_schema_by_title(
request: Request,
title: str | None = None,
):
if not title:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Provide exactly one of: id or title",
)

collection = request.app.state.schemas_collection
result = await collection.delete_one({"title": title})
if result.deleted_count == 0:
raise HTTPException(404, f"Schema with title `{title}` not found")

return {"message": "Schema deleted"}


@router.delete("/delete/id/{id}", status_code=status.HTTP_200_OK)
async def delete_schema_by_id(
request: Request,
id: str | None = None,
):
collection = request.app.state.schemas_collection
try:
oid = ObjectId(id)
except InvalidId:
raise HTTPException(400, "Invalid Mongo ObjectId")

result = await collection.delete_one({"_id": oid})
if result.deleted_count == 0:
raise HTTPException(404, f"Schema with id `{id}` not found")

return {"message": "Schema deleted"}


@router.put(
"/update/content/{id}",
response_model=UpdateResponse,
status_code=status.HTTP_200_OK,
)
async def update_schema_by_id(
request: Request,
id: str,
update: ContentUpdate,
):
collection = request.app.state.schemas_collection
try:
oid = ObjectId(id)
except InvalidId:
raise HTTPException(status_code=400, detail="Invalid Mongo ObjectId")

# Fetch current doc and hash
current = await collection.find_one({"_id": oid})
if current is None:
raise HTTPException(status_code=404, detail=f"Schema with id `{id}` not found")
old_hash = current.get("content_hash")

# Compute new hash
new_hash = hash_content(update.content)

# No change → skip update; return existing doc + message
if old_hash == new_hash:
current["_id"] = str(current["_id"])
return UpdateResponse(
updated=False,
schema=SchemaDefinition(**current),
message="Content unchanged; update skipped.",
)

# Content changed → perform update
updated = await collection.update_one(
{"_id": oid},
{
"$set": {
"content": update.content,
"content_hash": new_hash,
"updated_at": datetime.now(timezone.utc),
}
},
)
return UpdateResponse(
updated=True,
schema=SchemaDefinition(**updated),
message="Schema updated.",
)
29 changes: 29 additions & 0 deletions jsoned/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from fastapi import HTTPException
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorCollection

from jsoned.settings import settings

client: AsyncIOMotorClient | None = None
database = None
_schemas_collection: AsyncIOMotorCollection | None = None


async def connect_to_mongo():
global client, database, _schemas_collection

client = AsyncIOMotorClient(settings.MONGO_URI)
database = client[settings.MONGO_DB_NAME]
_schemas_collection = database[settings.SCHEMAS_COLLECTION_NAME]

await _schemas_collection.create_index("title", unique=True)


async def close_mongo():
if client is not None:
client.close()


def get_schemas_collection() -> AsyncIOMotorCollection:
if _schemas_collection is None:
raise HTTPException(503, "Database not available")
return _schemas_collection
34 changes: 34 additions & 0 deletions jsoned/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from contextlib import asynccontextmanager

from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware

from jsoned.api.v1.api import api_router
from jsoned.database import close_mongo, connect_to_mongo, get_schemas_collection
from jsoned.settings import settings


@asynccontextmanager
async def app_init(app: FastAPI):
await connect_to_mongo()
app.state.schemas_collection = get_schemas_collection()

app.include_router(api_router, prefix=settings.API_V1_STR)
yield
await close_mongo()


app = FastAPI(
title=settings.PROJECT_NAME,
lifespan=app_init,
)

# Set all CORS enabled origins
if settings.BACKEND_CORS_ORIGINS:
app.add_middleware(
CORSMiddleware,
allow_origins=[str(o) for o in settings.BACKEND_CORS_ORIGINS],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
Empty file added jsoned/models/__init__.py
Empty file.
Loading
Loading