Skip to content

Commit f1b9085

Browse files
committed
Added content update and hash
1 parent 411b196 commit f1b9085

File tree

2 files changed

+80
-50
lines changed

2 files changed

+80
-50
lines changed

jsoned/api/v1/endpoints/schemas.py

Lines changed: 54 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from datetime import datetime, timezone
2+
from typing import Any
23

34
from bson import ObjectId
45
from bson.errors import InvalidId
56
from fastapi import APIRouter, HTTPException, Request, status
67
from pymongo.errors import DuplicateKeyError
78

9+
from jsoned.models.content import ContentUpdate, UpdateResponse, hash_content
810
from jsoned.models.schema_definition import SchemaDefinition
911

1012
router = APIRouter()
@@ -25,9 +27,6 @@ async def get_all_schemas(request: Request):
2527
"/add", response_model=SchemaDefinition, status_code=status.HTTP_201_CREATED
2628
)
2729
async def add_schema(request: Request, schema: SchemaDefinition):
28-
"""
29-
Add a new schema. Requires `content` to be non-null. `updated_at` is always set by the server.
30-
"""
3130
if schema.content is None:
3231
raise HTTPException(
3332
status_code=status.HTTP_400_BAD_REQUEST,
@@ -39,6 +38,8 @@ async def add_schema(request: Request, schema: SchemaDefinition):
3938
# Build doc to insert, excluding None fields
4039
doc = schema.model_dump(exclude_none=True)
4140
doc["created_at"] = datetime.now(timezone.utc)
41+
doc["updated_at"] = doc["created_at"]
42+
doc["content_hash"] = hash_content(schema.content)
4243

4344
try:
4445
result = await collection.insert_one(doc)
@@ -57,10 +58,6 @@ async def delete_schema_by_title(
5758
request: Request,
5859
title: str | None = None,
5960
):
60-
"""
61-
Delete a schema by _id OR by title.
62-
Provide exactly one of: id or title.
63-
"""
6461
if not title:
6562
raise HTTPException(
6663
status_code=status.HTTP_400_BAD_REQUEST,
@@ -93,46 +90,53 @@ async def delete_schema_by_id(
9390
return {"message": "Schema deleted"}
9491

9592

96-
# @router.put("/update/{id}", response_model=dict[str, str])
97-
# async def update_schema(id: str, update: SchemaDefinition) -> dict[str, str]:
98-
# """
99-
# Update schema by `id`. Ignores `id` field in the payload (primary key is immutable).
100-
# Only non-None fields are updated; `updated_at` is refreshed automatically.
101-
# """
102-
# if not isinstance(id, str) or not id.strip():
103-
# raise HTTPException(
104-
# status_code=400, detail="Invalid schema id (must be a non-empty string)"
105-
# )
106-
107-
# # Ignore None values and prevent changing the primary key
108-
# update_fields = {
109-
# k: v for k, v in update.dict().items() if v is not None and k != "id"
110-
# }
111-
112-
# if update_fields:
113-
# update_fields["updated_at"] = datetime.utcnow()
114-
115-
# result = schemas_collection.update_one(
116-
# {"id": id}, {"$set": update_fields} if update_fields else {}
117-
# )
118-
# if result.matched_count == 0:
119-
# raise HTTPException(status_code=404, detail="Schema not found")
120-
121-
# return {"message": "Schema updated"}
122-
123-
124-
# @router.delete("/delete/{id}", response_model=dict[str, str])
125-
# async def delete_schema(id: str) -> dict[str, str]:
126-
# """
127-
# Delete schema by `id`.
128-
# """
129-
# if not isinstance(id, str) or not id.strip():
130-
# raise HTTPException(
131-
# status_code=400, detail="Invalid schema id (must be a non-empty string)"
132-
# )
133-
134-
# result = schemas_collection.delete_one({"id": id})
135-
# if result.deleted_count == 0:
136-
# raise HTTPException(status_code=404, detail="Schema not found")
137-
138-
# return {"message": "Schema deleted"}
93+
@router.put(
94+
"/update/content/{id}",
95+
response_model=UpdateResponse,
96+
status_code=status.HTTP_200_OK,
97+
)
98+
async def update_schema_by_id(
99+
request: Request,
100+
id: str,
101+
update: ContentUpdate,
102+
):
103+
collection = request.app.state.schemas_collection
104+
try:
105+
oid = ObjectId(id)
106+
except InvalidId:
107+
raise HTTPException(status_code=400, detail="Invalid Mongo ObjectId")
108+
109+
# Fetch current doc and hash
110+
current = await collection.find_one({"_id": oid})
111+
if current is None:
112+
raise HTTPException(status_code=404, detail=f"Schema with id `{id}` not found")
113+
old_hash = current.get("content_hash")
114+
115+
# Compute new hash
116+
new_hash = hash_content(update.content)
117+
118+
# No change → skip update; return existing doc + message
119+
if old_hash == new_hash:
120+
current["_id"] = str(current["_id"])
121+
return UpdateResponse(
122+
updated=False,
123+
schema=SchemaDefinition(**current),
124+
message="Content unchanged; update skipped.",
125+
)
126+
127+
# Content changed → perform update
128+
updated = await collection.update_one(
129+
{"_id": oid},
130+
{
131+
"$set": {
132+
"content": update.content,
133+
"content_hash": new_hash,
134+
"updated_at": datetime.now(timezone.utc),
135+
}
136+
},
137+
)
138+
return UpdateResponse(
139+
updated=True,
140+
schema=SchemaDefinition(**updated),
141+
message="Schema updated.",
142+
)

jsoned/models/content.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import hashlib
2+
import json
3+
from typing import Any
4+
5+
from pydantic import BaseModel, Field
6+
7+
from jsoned.models.schema_definition import SchemaDefinition
8+
9+
10+
def hash_content(content: dict[str, Any]) -> str:
11+
"""
12+
Stable hash for JSON-like dicts. Uses canonical JSON representation (sorted keys, no whitespace)
13+
and SHA-256 hashing.
14+
"""
15+
canonical = json.dumps(content, sort_keys=True, separators=(",", ":"))
16+
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
17+
18+
19+
class ContentUpdate(BaseModel):
20+
content: dict[str, Any] = Field(..., description="New schema content")
21+
22+
23+
class UpdateResponse(BaseModel):
24+
updated: bool
25+
schema: SchemaDefinition
26+
message: str

0 commit comments

Comments
 (0)