Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
cfa3505
NO change in version.py
GorkiPower Nov 28, 2025
4c3e908
add main.py; add model.py
GorkiPower Nov 28, 2025
9cc70a9
add: from datetime import datetime
GorkiPower Nov 28, 2025
8681ea8
add simple FARM-Stack
GorkiPower Nov 28, 2025
621e3fd
Ready to start -> uvicorn backend.main:app --reload
GorkiPower Nov 28, 2025
65b4a20
add
GorkiPower Nov 28, 2025
6f58f29
frontend is working without 404 Not Found
GorkiPower Nov 28, 2025
efebed4
Add Next.js frontend
GorkiPower Nov 28, 2025
4cf11e4
Starting Clean Backend Frontend NOT working together
GorkiPower Dec 1, 2025
ca11ef8
Server are NOT TALKING TO EACH OTHER
GorkiPower Dec 1, 2025
4494a01
Server a running - Frontend and Backend are NOT communiating
GorkiPower Dec 1, 2025
32f49ac
Update REAMDE
GorkiPower Dec 1, 2025
1568c6d
Updated README
GorkiPower Dec 1, 2025
8f057e1
ruff check .; ruff check . --fix; ruff format .; Add Ruff to pyprojec…
GorkiPower Dec 3, 2025
6ebd4f7
Update pyproject.toml
GorkiPower Dec 3, 2025
e83a983
Update: backend/_version.py
GorkiPower Dec 3, 2025
297ddf2
Apply Ruff formatting
GorkiPower Dec 3, 2025
f5dacc3
Update database.py, datamodel.py, main.py, model.py
GorkiPower Dec 3, 2025
05c1ce3
Updated datamodel.py main.py model.py
GorkiPower Dec 3, 2025
dcbf01e
run ruff check . --fix and ruff format .
GorkiPower Dec 3, 2025
1a728af
clean model.py
GorkiPower Dec 3, 2025
eb80f2f
bugs are fixed - ruff is happy
GorkiPower Dec 3, 2025
5c597aa
Merging model.py and datamodel.py has led to challenges :D Works now …
GorkiPower Dec 3, 2025
be2ef20
Clean all the rubbish
GorkiPower Dec 3, 2025
78a303e
delete model.py
GorkiPower Dec 3, 2025
fda8f59
Update: main before more verbose
GorkiPower Dec 3, 2025
db59966
It works fine
GorkiPower Dec 3, 2025
b8d2af9
BugFix: ruff check . --fix; ruff format .
GorkiPower Dec 4, 2025
46c4338
Update main.py
GorkiPower Dec 8, 2025
158dff2
feat: use jsonable_encoder with custom ObjectId encoder for clean RES…
GorkiPower Dec 8, 2025
9a8e751
SchemaDefinition with Optional Fields
GorkiPower Dec 8, 2025
f977b81
Refactored main.py 🔑 id: str ~ improve clarity and prepare for future…
GorkiPower Dec 9, 2025
93d86eb
Updated backend/datamodel.py
GorkiPower Dec 9, 2025
bc4e036
Refactored/CleanUp main.py: using list[dict[str, Any]] for Python(3.9+)
GorkiPower Dec 9, 2025
3b17c4a
Cleanup datamodel.py
GorkiPower Dec 9, 2025
9895288
Files cleaned up
GorkiPower Dec 9, 2025
720e9b1
Changed backend to jsoned
JosePizarro3 Dec 10, 2025
2bfd6e1
Moved router functions to api/api_v1/endpoints
JosePizarro3 Dec 10, 2025
a67375e
Polishing schemas endpoint only including the get_all_schemas method
JosePizarro3 Dec 10, 2025
9e88b8e
Clean up settings.py
JosePizarro3 Dec 10, 2025
e204af3
Fix bug with create_index for schema_collections
JosePizarro3 Dec 10, 2025
7397ba6
Renamed from api_v1 to v1
JosePizarro3 Dec 10, 2025
6e1c15b
Fix error with schemas_collection
JosePizarro3 Dec 10, 2025
1f85883
Added created_at
JosePizarro3 Dec 10, 2025
411b196
Added delete methods
JosePizarro3 Dec 10, 2025
f1b9085
Added content update and hash
JosePizarro3 Dec 11, 2025
f2cec21
Added content_hash field
JosePizarro3 Dec 11, 2025
621de80
Delete unused import
JosePizarro3 Dec 11, 2025
c685bcd
Added MONGO_DB_NAME and SCHEMAS_COLLECTION_NAME to settings
JosePizarro3 Dec 11, 2025
598ce7d
Changed README
JosePizarro3 Dec 17, 2025
e6b03c8
Deleted React GUI
JosePizarro3 Dec 17, 2025
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
13 changes: 13 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Application
PROJECT_NAME=JSONed
API_V1_STR=/api/v1

# MongoDB
MONGO_DATABASE=jsoned_db
MONGO_DATABASE_URI=mongodb://localhost:27017
COLLECTION=schemas

# CORS (comma-separated or JSON list)
BACKEND_CORS_ORIGINS=http://localhost,http://localhost:3000
# or:
# BACKEND_CORS_ORIGINS=["http://localhost","http://localhost:3000"]
14 changes: 12 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# added
.MyNotes/
.venv_jsoned


# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
Expand Down Expand Up @@ -182,9 +187,9 @@ cython_debug/
.abstra/

# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/

Expand All @@ -205,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.

60 changes: 59 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,60 @@
# jsoned
A full-stack application to visualize and edit entities and their relations defined in JSON Schema.

A local application to visualize and edit entities and their relations defined in a JSON Schema. The architecture
of `jsoned` is:

- FastAPI (Python): backend
- SvelteKit (JavaScript): frontend
- MongoDB: database
- Tauri (Rust): framework for building binaries for desktop app


## Development

If you want to develop locally this package, clone the project and enter in the workspace folder:

```sh
git clone https://github.com/BAMresearch/jsoned.git
cd jsoned
```

Create a virtual environment (you can use Python>3.10) in your workspace:

```sh
python3 -m venv .venv
source .venv/bin/activate
```

We recommend using [`uv`](https://docs.astral.sh/uv/) for installing the dependencies:

```sh
uv sync
```

### Run the app

In order to run the app, you need to follow the instructions for different services. Using `uvicorn` you can launch the FastAPI app:

```sh
cd jsoned/
uvicorn main:app --reload
```

The SwaggerUI will help you understand the implemented endpoints.

#### MongoDB Compass

Go to [MongoDB](https://www.mongodb.com/) and install [MongoDB Community Edition](https://www.mongodb.com/docs/manual/administration/install-community/?operating-system=linux&linux-distribution=ubuntu&linux-package=default&search-linux=with-search-linux) and [MongoDB Compass](https://www.mongodb.com/try/download/compass).

Once the installation is finished, launch MongoDB Compass and start a connection with URI `mongodb://localhost:27017`. Name it `json_db`. You can also create a new collection and call it `schemas`. The URI and names of the database and collection are defined in `jsoned/settings.py`.

#### NodeJS

We use `npm` to manage the frontend dependencies. Go to [NodeJS](https://nodejs.org/en) and install `npm`.

You can install and run the SvelteKit server:

```sh
cd gui/
npm run dev
```
9 changes: 0 additions & 9 deletions backend/datamodel.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_DATABASE_URI)
database = client[settings.MONGO_DATABASE]
_schemas_collection = database[settings.COLLECTION]

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.
26 changes: 26 additions & 0 deletions jsoned/models/content.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import hashlib
import json
from typing import Any

from pydantic import BaseModel, Field

from jsoned.models.schema_definition import SchemaDefinition


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 ContentUpdate(BaseModel):
content: dict[str, Any] = Field(..., description="New schema content")


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