Skip to content
Merged
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
40 changes: 22 additions & 18 deletions jsoned/api/v1/endpoints/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@
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
from jsoned.models.schema_definition import (
ContentUpdate,
SchemaCreate,
SchemaDefinition,
UpdateResponse,
hash_content,
)

router = APIRouter()

Expand All @@ -25,20 +30,18 @@ async def get_all_schemas(request: Request):
@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.",
)

async def add_schema(request: Request, payload: SchemaCreate):
collection = request.app.state.schemas_collection
now = datetime.now(timezone.utc)

# 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)
doc = {
"title": payload.title,
"content": payload.content,
"created_at": now,
"updated_at": now,
"content_hash": hash_content(payload.content),
}

try:
result = await collection.insert_one(doc)
Expand All @@ -55,7 +58,7 @@ async def add_schema(request: Request, schema: SchemaDefinition):
@router.delete("/delete/{title}", status_code=status.HTTP_200_OK)
async def delete_schema_by_title(
request: Request,
title: str | None = None,
title: str,
):
if not title:
raise HTTPException(
Expand All @@ -74,7 +77,7 @@ async def delete_schema_by_title(
@router.delete("/delete/id/{id}", status_code=status.HTTP_200_OK)
async def delete_schema_by_id(
request: Request,
id: str | None = None,
id: str,
):
collection = request.app.state.schemas_collection
try:
Expand Down Expand Up @@ -109,9 +112,9 @@ async def update_schema_by_id(
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
# hashes
old_hash = current.get("content_hash")
new_hash = hash_content(update.content)

# No change → skip update; return existing doc + message
Expand All @@ -124,7 +127,7 @@ async def update_schema_by_id(
)

# Content changed → perform update
updated = await collection.update_one(
updated = await collection.find_one_and_update(
{"_id": oid},
{
"$set": {
Expand All @@ -133,6 +136,7 @@ async def update_schema_by_id(
"updated_at": datetime.now(timezone.utc),
}
},
return_document=True,
)
return UpdateResponse(
updated=True,
Expand Down
26 changes: 0 additions & 26 deletions jsoned/models/content.py

This file was deleted.

43 changes: 38 additions & 5 deletions jsoned/models/schema_definition.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,32 @@
import hashlib
import json
from datetime import datetime
from typing import Any

from pydantic import BaseModel, ConfigDict, Field


def hash_content(content: dict[str, Any]) -> str:
"""
Stable hash for JSON-like dicts. Uses canonical JSON representation (sorted keys, no whitespace)
and SHA-256 hashing.
"""
canonical = json.dumps(content, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()


class SchemaCreate(BaseModel):
title: str = Field(
...,
description="A human-readable title given to the schema entry.",
min_length=3,
)
content: dict | None = Field(
None,
description="The actual schema content as a dictionary",
)


class SchemaDefinition(BaseModel):
model_config = ConfigDict(populate_by_name=True)

Expand All @@ -13,6 +37,10 @@ class SchemaDefinition(BaseModel):
description="A human-readable title given to the schema entry.",
min_length=3,
)
content: dict | None = Field(
None,
description="The actual schema content as a dictionary",
)

created_at: datetime | None = Field(
None,
Expand All @@ -24,12 +52,17 @@ class SchemaDefinition(BaseModel):
description="Timestamp of the last update of the schema entry.",
)

content: dict | None = Field(
None,
description="The actual schema content as a dictionary",
)

content_hash: str | None = Field(
None,
description="A stable hash of the schema content for integrity verification",
)


class ContentUpdate(BaseModel):
content: dict[str, Any] = Field(..., description="New schema content")


class UpdateResponse(BaseModel):
updated: bool
schema: SchemaDefinition
message: str
Loading