11from datetime import datetime , timezone
2+ from typing import Any
23
34from bson import ObjectId
45from bson .errors import InvalidId
56from fastapi import APIRouter , HTTPException , Request , status
67from pymongo .errors import DuplicateKeyError
78
9+ from jsoned .models .content import ContentUpdate , UpdateResponse , hash_content
810from jsoned .models .schema_definition import SchemaDefinition
911
1012router = APIRouter ()
@@ -25,9 +27,6 @@ async def get_all_schemas(request: Request):
2527 "/add" , response_model = SchemaDefinition , status_code = status .HTTP_201_CREATED
2628)
2729async 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+ )
0 commit comments