Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a14fcc4ca | |||
| fc4cb0273e | |||
| 83f83ab641 | |||
| 064bb6ea65 | |||
| fbb9d0195c |
@@ -2,7 +2,16 @@
|
|||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from app.api.v1 import auth, events, persons, relationships, trees, users
|
from app.api.v1 import (
|
||||||
|
auth,
|
||||||
|
citations,
|
||||||
|
events,
|
||||||
|
persons,
|
||||||
|
relationships,
|
||||||
|
sources,
|
||||||
|
trees,
|
||||||
|
users,
|
||||||
|
)
|
||||||
|
|
||||||
api_router = APIRouter(prefix="/api/v1")
|
api_router = APIRouter(prefix="/api/v1")
|
||||||
api_router.include_router(auth.router)
|
api_router.include_router(auth.router)
|
||||||
@@ -11,3 +20,5 @@ api_router.include_router(trees.router)
|
|||||||
api_router.include_router(persons.router)
|
api_router.include_router(persons.router)
|
||||||
api_router.include_router(events.router)
|
api_router.include_router(events.router)
|
||||||
api_router.include_router(relationships.router)
|
api_router.include_router(relationships.router)
|
||||||
|
api_router.include_router(sources.router)
|
||||||
|
api_router.include_router(citations.router)
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from fastapi import APIRouter, status
|
||||||
|
|
||||||
|
from app.api.deps import CurrentUser, SessionDep
|
||||||
|
from app.schemas.source import CitationCreate, CitationRead
|
||||||
|
from app.services import citation_service, tree_service
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/trees", tags=["citations"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/{tree_id}/citations", response_model=CitationRead, status_code=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
|
async def create_citation(
|
||||||
|
tree_id: uuid.UUID, data: CitationCreate, session: SessionDep, current: CurrentUser
|
||||||
|
) -> CitationRead:
|
||||||
|
tree = await tree_service.get_tree(session, viewer_id=current.id, tree_id=tree_id)
|
||||||
|
citation = await citation_service.create_citation(
|
||||||
|
session, actor=current, tree=tree, **data.model_dump()
|
||||||
|
)
|
||||||
|
return CitationRead.model_validate(citation)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{tree_id}/citations", response_model=list[CitationRead])
|
||||||
|
async def list_citations(
|
||||||
|
tree_id: uuid.UUID, session: SessionDep, current: CurrentUser
|
||||||
|
) -> list[CitationRead]:
|
||||||
|
tree = await tree_service.get_tree(session, viewer_id=current.id, tree_id=tree_id)
|
||||||
|
citations = await citation_service.list_citations(session, viewer_id=current.id, tree=tree)
|
||||||
|
return [CitationRead.model_validate(c) for c in citations]
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{tree_id}/citations/{citation_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
async def delete_citation(
|
||||||
|
tree_id: uuid.UUID, citation_id: uuid.UUID, session: SessionDep, current: CurrentUser
|
||||||
|
) -> None:
|
||||||
|
tree = await tree_service.get_tree(session, viewer_id=current.id, tree_id=tree_id)
|
||||||
|
await citation_service.delete_citation(
|
||||||
|
session, actor=current, tree=tree, citation_id=citation_id
|
||||||
|
)
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from fastapi import APIRouter, status
|
||||||
|
|
||||||
|
from app.api.deps import CurrentUser, SessionDep
|
||||||
|
from app.schemas.source import SourceCreate, SourceRead
|
||||||
|
from app.services import source_service, tree_service
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/trees", tags=["sources"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{tree_id}/sources", response_model=SourceRead, status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_source(
|
||||||
|
tree_id: uuid.UUID, data: SourceCreate, session: SessionDep, current: CurrentUser
|
||||||
|
) -> SourceRead:
|
||||||
|
tree = await tree_service.get_tree(session, viewer_id=current.id, tree_id=tree_id)
|
||||||
|
source = await source_service.create_source(
|
||||||
|
session, actor=current, tree=tree, **data.model_dump()
|
||||||
|
)
|
||||||
|
return SourceRead.model_validate(source)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{tree_id}/sources", response_model=list[SourceRead])
|
||||||
|
async def list_sources(
|
||||||
|
tree_id: uuid.UUID, session: SessionDep, current: CurrentUser
|
||||||
|
) -> list[SourceRead]:
|
||||||
|
tree = await tree_service.get_tree(session, viewer_id=current.id, tree_id=tree_id)
|
||||||
|
sources = await source_service.list_sources(session, viewer_id=current.id, tree=tree)
|
||||||
|
return [SourceRead.model_validate(s) for s in sources]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{tree_id}/sources/{source_id}", response_model=SourceRead)
|
||||||
|
async def get_source(
|
||||||
|
tree_id: uuid.UUID, source_id: uuid.UUID, session: SessionDep, current: CurrentUser
|
||||||
|
) -> SourceRead:
|
||||||
|
tree = await tree_service.get_tree(session, viewer_id=current.id, tree_id=tree_id)
|
||||||
|
source = await source_service.get_source(
|
||||||
|
session, viewer_id=current.id, tree=tree, source_id=source_id
|
||||||
|
)
|
||||||
|
return SourceRead.model_validate(source)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{tree_id}/sources/{source_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
async def delete_source(
|
||||||
|
tree_id: uuid.UUID, source_id: uuid.UUID, session: SessionDep, current: CurrentUser
|
||||||
|
) -> None:
|
||||||
|
tree = await tree_service.get_tree(session, viewer_id=current.id, tree_id=tree_id)
|
||||||
|
await source_service.delete_source(session, actor=current, tree=tree, source_id=source_id)
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
|
from app.models.enums import CitationConfidence
|
||||||
|
|
||||||
|
|
||||||
|
class SourceCreate(BaseModel):
|
||||||
|
title: str
|
||||||
|
author: str | None = None
|
||||||
|
source_type: str | None = None
|
||||||
|
repository: str | None = None
|
||||||
|
url: str | None = None
|
||||||
|
citation_text: str | None = None
|
||||||
|
publication_info: str | None = None
|
||||||
|
quality_note: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class SourceRead(BaseModel):
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
id: uuid.UUID
|
||||||
|
tree_id: uuid.UUID
|
||||||
|
title: str
|
||||||
|
author: str | None
|
||||||
|
source_type: str | None
|
||||||
|
repository: str | None
|
||||||
|
url: str | None
|
||||||
|
citation_text: str | None
|
||||||
|
publication_info: str | None
|
||||||
|
quality_note: str | None
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class CitationCreate(BaseModel):
|
||||||
|
source_id: uuid.UUID
|
||||||
|
# Exactly one target fact.
|
||||||
|
person_id: uuid.UUID | None = None
|
||||||
|
event_id: uuid.UUID | None = None
|
||||||
|
name_id: uuid.UUID | None = None
|
||||||
|
relationship_id: uuid.UUID | None = None
|
||||||
|
page: str | None = None
|
||||||
|
detail: str | None = None
|
||||||
|
confidence: CitationConfidence | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class CitationRead(BaseModel):
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
id: uuid.UUID
|
||||||
|
tree_id: uuid.UUID
|
||||||
|
source_id: uuid.UUID
|
||||||
|
person_id: uuid.UUID | None
|
||||||
|
event_id: uuid.UUID | None
|
||||||
|
name_id: uuid.UUID | None
|
||||||
|
relationship_id: uuid.UUID | None
|
||||||
|
page: str | None
|
||||||
|
detail: str | None
|
||||||
|
confidence: CitationConfidence | None
|
||||||
|
created_at: datetime
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
"""Citation service. A citation links one Source to exactly one fact (person,
|
||||||
|
event, name, or relationship) within a tree — the provenance spine."""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.models.enums import CitationConfidence
|
||||||
|
from app.models.event import Event
|
||||||
|
from app.models.person import Name, Person
|
||||||
|
from app.models.relationship import Relationship
|
||||||
|
from app.models.source import Citation, Source
|
||||||
|
from app.models.tree import Tree
|
||||||
|
from app.models.user import User
|
||||||
|
from app.services import privacy
|
||||||
|
from app.services.audit import record_audit
|
||||||
|
from app.services.exceptions import Conflict, Forbidden, NotFound
|
||||||
|
|
||||||
|
# Citation target column -> model, for tenant/existence validation.
|
||||||
|
_TARGET_MODELS = {
|
||||||
|
"person_id": Person,
|
||||||
|
"event_id": Event,
|
||||||
|
"name_id": Name,
|
||||||
|
"relationship_id": Relationship,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _in_tree(session: AsyncSession, model: type, id_: uuid.UUID, tree_id: uuid.UUID) -> bool:
|
||||||
|
row = (
|
||||||
|
await session.execute(
|
||||||
|
select(model.id).where(
|
||||||
|
model.id == id_, model.tree_id == tree_id, model.deleted_at.is_(None)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
return row is not None
|
||||||
|
|
||||||
|
|
||||||
|
async def create_citation(
|
||||||
|
session: AsyncSession,
|
||||||
|
*,
|
||||||
|
actor: User,
|
||||||
|
tree: Tree,
|
||||||
|
source_id: uuid.UUID,
|
||||||
|
person_id: uuid.UUID | None = None,
|
||||||
|
event_id: uuid.UUID | None = None,
|
||||||
|
name_id: uuid.UUID | None = None,
|
||||||
|
relationship_id: uuid.UUID | None = None,
|
||||||
|
page: str | None = None,
|
||||||
|
detail: str | None = None,
|
||||||
|
confidence: CitationConfidence | None = None,
|
||||||
|
) -> Citation:
|
||||||
|
if not await privacy.can_edit_tree(session, user_id=actor.id, tree=tree):
|
||||||
|
raise Forbidden("not an editor of this tree")
|
||||||
|
|
||||||
|
targets = {
|
||||||
|
"person_id": person_id,
|
||||||
|
"event_id": event_id,
|
||||||
|
"name_id": name_id,
|
||||||
|
"relationship_id": relationship_id,
|
||||||
|
}
|
||||||
|
set_targets = {k: v for k, v in targets.items() if v is not None}
|
||||||
|
if len(set_targets) != 1:
|
||||||
|
raise Conflict("a citation must reference exactly one fact")
|
||||||
|
|
||||||
|
if not await _in_tree(session, Source, source_id, tree.id):
|
||||||
|
raise NotFound("source not found in this tree")
|
||||||
|
(target_col, target_id), = set_targets.items()
|
||||||
|
if not await _in_tree(session, _TARGET_MODELS[target_col], target_id, tree.id):
|
||||||
|
raise NotFound("cited fact not found in this tree")
|
||||||
|
|
||||||
|
citation = Citation(
|
||||||
|
tree_id=tree.id,
|
||||||
|
source_id=source_id,
|
||||||
|
person_id=person_id,
|
||||||
|
event_id=event_id,
|
||||||
|
name_id=name_id,
|
||||||
|
relationship_id=relationship_id,
|
||||||
|
page=page,
|
||||||
|
detail=detail,
|
||||||
|
confidence=confidence,
|
||||||
|
)
|
||||||
|
session.add(citation)
|
||||||
|
await session.flush()
|
||||||
|
record_audit(
|
||||||
|
session,
|
||||||
|
action="create",
|
||||||
|
entity_type="Citation",
|
||||||
|
entity_id=citation.id,
|
||||||
|
tree_id=tree.id,
|
||||||
|
actor_user_id=actor.id,
|
||||||
|
after={"source_id": str(source_id), target_col: str(target_id)},
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(citation)
|
||||||
|
return citation
|
||||||
|
|
||||||
|
|
||||||
|
async def list_citations(
|
||||||
|
session: AsyncSession, *, viewer_id: uuid.UUID, tree: Tree
|
||||||
|
) -> list[Citation]:
|
||||||
|
"""All citations in the tree — the UI maps them to facts to show 'sourced'
|
||||||
|
indicators in a single round-trip."""
|
||||||
|
if not await privacy.can_view_tree(session, user_id=viewer_id, tree=tree):
|
||||||
|
raise Forbidden("not permitted to view this tree")
|
||||||
|
stmt = (
|
||||||
|
select(Citation)
|
||||||
|
.where(Citation.tree_id == tree.id, Citation.deleted_at.is_(None))
|
||||||
|
.order_by(Citation.created_at)
|
||||||
|
)
|
||||||
|
return list((await session.execute(stmt)).scalars().all())
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_citation(
|
||||||
|
session: AsyncSession, *, actor: User, tree: Tree, citation_id: uuid.UUID
|
||||||
|
) -> None:
|
||||||
|
if not await privacy.can_edit_tree(session, user_id=actor.id, tree=tree):
|
||||||
|
raise Forbidden("not an editor of this tree")
|
||||||
|
citation = (
|
||||||
|
await session.execute(
|
||||||
|
select(Citation).where(
|
||||||
|
Citation.id == citation_id,
|
||||||
|
Citation.tree_id == tree.id,
|
||||||
|
Citation.deleted_at.is_(None),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
if citation is None:
|
||||||
|
raise NotFound("citation not found")
|
||||||
|
citation.deleted_at = datetime.now(UTC)
|
||||||
|
record_audit(
|
||||||
|
session,
|
||||||
|
action="delete",
|
||||||
|
entity_type="Citation",
|
||||||
|
entity_id=citation.id,
|
||||||
|
tree_id=tree.id,
|
||||||
|
actor_user_id=actor.id,
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
"""Source service. Sources are reusable, tree-scoped records of an origin.
|
||||||
|
Writes require editor rights; reads go through the privacy engine."""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.models.source import Source
|
||||||
|
from app.models.tree import Tree
|
||||||
|
from app.models.user import User
|
||||||
|
from app.services import privacy
|
||||||
|
from app.services.audit import record_audit
|
||||||
|
from app.services.exceptions import Forbidden, NotFound
|
||||||
|
|
||||||
|
|
||||||
|
async def create_source(
|
||||||
|
session: AsyncSession,
|
||||||
|
*,
|
||||||
|
actor: User,
|
||||||
|
tree: Tree,
|
||||||
|
title: str,
|
||||||
|
author: str | None = None,
|
||||||
|
source_type: str | None = None,
|
||||||
|
repository: str | None = None,
|
||||||
|
url: str | None = None,
|
||||||
|
citation_text: str | None = None,
|
||||||
|
publication_info: str | None = None,
|
||||||
|
quality_note: str | None = None,
|
||||||
|
) -> Source:
|
||||||
|
if not await privacy.can_edit_tree(session, user_id=actor.id, tree=tree):
|
||||||
|
raise Forbidden("not an editor of this tree")
|
||||||
|
source = Source(
|
||||||
|
tree_id=tree.id,
|
||||||
|
title=title,
|
||||||
|
author=author,
|
||||||
|
source_type=source_type,
|
||||||
|
repository=repository,
|
||||||
|
url=url,
|
||||||
|
citation_text=citation_text,
|
||||||
|
publication_info=publication_info,
|
||||||
|
quality_note=quality_note,
|
||||||
|
)
|
||||||
|
session.add(source)
|
||||||
|
await session.flush()
|
||||||
|
record_audit(
|
||||||
|
session,
|
||||||
|
action="create",
|
||||||
|
entity_type="Source",
|
||||||
|
entity_id=source.id,
|
||||||
|
tree_id=tree.id,
|
||||||
|
actor_user_id=actor.id,
|
||||||
|
after={"title": title},
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(source)
|
||||||
|
return source
|
||||||
|
|
||||||
|
|
||||||
|
async def list_sources(session: AsyncSession, *, viewer_id: uuid.UUID, tree: Tree) -> list[Source]:
|
||||||
|
if not await privacy.can_view_tree(session, user_id=viewer_id, tree=tree):
|
||||||
|
raise Forbidden("not permitted to view this tree")
|
||||||
|
stmt = (
|
||||||
|
select(Source)
|
||||||
|
.where(Source.tree_id == tree.id, Source.deleted_at.is_(None))
|
||||||
|
.order_by(Source.title)
|
||||||
|
)
|
||||||
|
return list((await session.execute(stmt)).scalars().all())
|
||||||
|
|
||||||
|
|
||||||
|
async def get_source(
|
||||||
|
session: AsyncSession, *, viewer_id: uuid.UUID, tree: Tree, source_id: uuid.UUID
|
||||||
|
) -> Source:
|
||||||
|
if not await privacy.can_view_tree(session, user_id=viewer_id, tree=tree):
|
||||||
|
raise Forbidden("not permitted to view this tree")
|
||||||
|
source = (
|
||||||
|
await session.execute(
|
||||||
|
select(Source).where(
|
||||||
|
Source.id == source_id, Source.tree_id == tree.id, Source.deleted_at.is_(None)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
if source is None:
|
||||||
|
raise NotFound("source not found")
|
||||||
|
return source
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_source(
|
||||||
|
session: AsyncSession, *, actor: User, tree: Tree, source_id: uuid.UUID
|
||||||
|
) -> None:
|
||||||
|
if not await privacy.can_edit_tree(session, user_id=actor.id, tree=tree):
|
||||||
|
raise Forbidden("not an editor of this tree")
|
||||||
|
source = (
|
||||||
|
await session.execute(
|
||||||
|
select(Source).where(
|
||||||
|
Source.id == source_id, Source.tree_id == tree.id, Source.deleted_at.is_(None)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
if source is None:
|
||||||
|
raise NotFound("source not found")
|
||||||
|
source.deleted_at = datetime.now(UTC)
|
||||||
|
record_audit(
|
||||||
|
session,
|
||||||
|
action="delete",
|
||||||
|
entity_type="Source",
|
||||||
|
entity_id=source.id,
|
||||||
|
tree_id=tree.id,
|
||||||
|
actor_user_id=actor.id,
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
"""Sources and citations (the provenance spine)."""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from tests.conftest import auth, register
|
||||||
|
|
||||||
|
|
||||||
|
async def _setup(client, email):
|
||||||
|
h = auth(await register(client, email))
|
||||||
|
tree_id = (await client.post("/api/v1/trees", json={"name": "S"}, headers=h)).json()["id"]
|
||||||
|
person = (
|
||||||
|
await client.post(
|
||||||
|
f"/api/v1/trees/{tree_id}/persons", json={"given": "Cy"}, headers=h
|
||||||
|
)
|
||||||
|
).json()["id"]
|
||||||
|
event = (
|
||||||
|
await client.post(
|
||||||
|
f"/api/v1/trees/{tree_id}/events",
|
||||||
|
json={"event_type": "birth", "person_id": person},
|
||||||
|
headers=h,
|
||||||
|
)
|
||||||
|
).json()["id"]
|
||||||
|
return h, tree_id, person, event
|
||||||
|
|
||||||
|
|
||||||
|
async def test_source_and_citation_flow(client):
|
||||||
|
h, tree_id, person, event = await _setup(client, "src1@example.com")
|
||||||
|
|
||||||
|
src = await client.post(
|
||||||
|
f"/api/v1/trees/{tree_id}/sources",
|
||||||
|
json={"title": "1880 Census", "repository": "NARA"},
|
||||||
|
headers=h,
|
||||||
|
)
|
||||||
|
assert src.status_code == 201, src.text
|
||||||
|
source_id = src.json()["id"]
|
||||||
|
|
||||||
|
assert len((await client.get(f"/api/v1/trees/{tree_id}/sources", headers=h)).json()) == 1
|
||||||
|
|
||||||
|
# Cite the source on the birth event.
|
||||||
|
cite = await client.post(
|
||||||
|
f"/api/v1/trees/{tree_id}/citations",
|
||||||
|
json={"source_id": source_id, "event_id": event, "page": "p. 4"},
|
||||||
|
headers=h,
|
||||||
|
)
|
||||||
|
assert cite.status_code == 201, cite.text
|
||||||
|
citation_id = cite.json()["id"]
|
||||||
|
|
||||||
|
citations = (await client.get(f"/api/v1/trees/{tree_id}/citations", headers=h)).json()
|
||||||
|
assert len(citations) == 1
|
||||||
|
assert citations[0]["event_id"] == event
|
||||||
|
assert citations[0]["source_id"] == source_id
|
||||||
|
|
||||||
|
resp = await client.delete(f"/api/v1/trees/{tree_id}/citations/{citation_id}", headers=h)
|
||||||
|
assert resp.status_code == 204
|
||||||
|
assert len((await client.get(f"/api/v1/trees/{tree_id}/citations", headers=h)).json()) == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_citation_needs_exactly_one_target(client):
|
||||||
|
h, tree_id, person, event = await _setup(client, "src2@example.com")
|
||||||
|
source_id = (
|
||||||
|
await client.post(
|
||||||
|
f"/api/v1/trees/{tree_id}/sources", json={"title": "X"}, headers=h
|
||||||
|
)
|
||||||
|
).json()["id"]
|
||||||
|
|
||||||
|
# No target.
|
||||||
|
r = await client.post(
|
||||||
|
f"/api/v1/trees/{tree_id}/citations", json={"source_id": source_id}, headers=h
|
||||||
|
)
|
||||||
|
assert r.status_code == 409
|
||||||
|
# Two targets.
|
||||||
|
r = await client.post(
|
||||||
|
f"/api/v1/trees/{tree_id}/citations",
|
||||||
|
json={"source_id": source_id, "person_id": person, "event_id": event},
|
||||||
|
headers=h,
|
||||||
|
)
|
||||||
|
assert r.status_code == 409
|
||||||
|
|
||||||
|
|
||||||
|
async def test_citation_unknown_source_404(client):
|
||||||
|
h, tree_id, person, _ = await _setup(client, "src3@example.com")
|
||||||
|
r = await client.post(
|
||||||
|
f"/api/v1/trees/{tree_id}/citations",
|
||||||
|
json={"source_id": str(uuid.uuid4()), "person_id": person},
|
||||||
|
headers=h,
|
||||||
|
)
|
||||||
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
async def test_non_member_cannot_create_source(client):
|
||||||
|
h, tree_id, _, _ = await _setup(client, "src4@example.com")
|
||||||
|
other = auth(await register(client, "src-intruder@example.com"))
|
||||||
|
r = await client.post(
|
||||||
|
f"/api/v1/trees/{tree_id}/sources", json={"title": "nope"}, headers=other
|
||||||
|
)
|
||||||
|
assert r.status_code == 403
|
||||||
+26
-14
@@ -1,44 +1,56 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
/* Brand palette (docs/brand): warm ink + bronze + paper. */
|
/* Brand palette + type (docs/brand): warm ink + bronze + paper, serif display. */
|
||||||
@theme {
|
@theme {
|
||||||
--color-bronze: #a06a42;
|
--color-bronze: #a06a42;
|
||||||
--color-bronze-deep: #8a5836;
|
--color-bronze-deep: #8a5836;
|
||||||
--color-paper: #f7f3ec;
|
--color-paper: #f7f3ec;
|
||||||
--color-ink: #1a1a17;
|
--color-ink: #1a1a17;
|
||||||
|
|
||||||
--font-serif: Georgia, "Times New Roman", "Liberation Serif", ui-serif, serif;
|
--font-sans: var(--font-inter), ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||||
|
--font-serif: var(--font-fraunces), Georgia, "Times New Roman", ui-serif, serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Adaptive tokens (ink/paper flip for light/dark; bronze + paper are constant). */
|
/* Adaptive tokens — ink/paper flip for light/dark; bronze + paper are constant. */
|
||||||
:root {
|
:root {
|
||||||
--background: #f7f3ec; /* paper */
|
--background: #f7f3ec;
|
||||||
--foreground: #1a1a17; /* ink */
|
--foreground: #1a1a17;
|
||||||
--muted: #6b6862;
|
--muted: #6b6862;
|
||||||
--surface: #fbf8f2;
|
--surface: #fffdf9;
|
||||||
--border: #e4dccb;
|
--border: #e6ddcc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
--background: #1a1a17; /* warm near-black */
|
--background: #161410;
|
||||||
--foreground: #f2eee6; /* warm off-white */
|
--foreground: #f2eee6;
|
||||||
--muted: #9a968e;
|
--muted: #9a968e;
|
||||||
--surface: #232019;
|
--surface: #211d17;
|
||||||
--border: #3a352c;
|
--border: #353029;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--background);
|
/* A faint bronze warmth pooled at the top gives the flat paper some depth. */
|
||||||
|
background:
|
||||||
|
radial-gradient(
|
||||||
|
1100px 520px at 50% -8%,
|
||||||
|
color-mix(in srgb, var(--color-bronze) 9%, var(--background)),
|
||||||
|
var(--background) 60%
|
||||||
|
);
|
||||||
|
background-attachment: fixed;
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
font-family: var(--font-sans);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Headings use the heritage serif register. */
|
|
||||||
h1,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
h3,
|
h3,
|
||||||
.font-serif {
|
.font-serif {
|
||||||
font-family: var(--font-serif);
|
font-family: var(--font-serif);
|
||||||
|
letter-spacing: -0.015em;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: color-mix(in srgb, var(--color-bronze) 22%, transparent);
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-12
@@ -1,38 +1,55 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
import { Fraunces, Inter } from "next/font/google";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
|
// Heritage display serif + clean humanist sans (per docs/brand typography).
|
||||||
|
const serif = Fraunces({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "--font-fraunces",
|
||||||
|
display: "swap",
|
||||||
|
axes: ["opsz"],
|
||||||
|
});
|
||||||
|
const sans = Inter({ subsets: ["latin"], variable: "--font-inter", display: "swap" });
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Provenance",
|
title: "Provenance — where it came from matters",
|
||||||
description: "Where it came from matters — family and land, every fact sourced.",
|
description:
|
||||||
|
"Trace your family and your land in one place — every fact linked to the record it came from. Self-hosted, sourced, and yours to keep.",
|
||||||
icons: { icon: "/favicon.svg" },
|
icons: { icon: "/favicon.svg" },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en" className={`${serif.variable} ${sans.variable}`}>
|
||||||
<body className="flex min-h-screen flex-col">
|
<body className="flex min-h-screen flex-col antialiased">
|
||||||
<header className="border-b border-[var(--border)]">
|
<header className="sticky top-0 z-20 border-b border-[var(--border)] bg-[var(--background)]">
|
||||||
<div className="mx-auto flex max-w-3xl items-center justify-between px-4 py-3">
|
<div className="mx-auto flex max-w-5xl items-center justify-between px-5 py-3.5">
|
||||||
<Link href="/" className="flex items-center" aria-label="Provenance — home">
|
<Link href="/" className="flex items-center" aria-label="Provenance — home">
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
<img src="/provenance-logo-plain.svg" alt="Provenance" className="h-7 w-auto" />
|
<img src="/provenance-logo-plain.svg" alt="Provenance" className="h-7 w-auto" />
|
||||||
</Link>
|
</Link>
|
||||||
<nav className="flex gap-5 text-sm">
|
<nav className="flex items-center gap-6 text-sm">
|
||||||
<Link href="/trees" className="text-[var(--muted)] transition-colors hover:text-bronze">
|
<Link href="/trees" className="text-[var(--muted)] transition-colors hover:text-[var(--foreground)]">
|
||||||
Trees
|
Trees
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/login" className="text-[var(--muted)] transition-colors hover:text-bronze">
|
<Link
|
||||||
|
href="/login"
|
||||||
|
className="rounded-full border border-[var(--border)] px-4 py-1.5 font-medium transition-colors hover:border-bronze hover:text-bronze"
|
||||||
|
>
|
||||||
Sign in
|
Sign in
|
||||||
</Link>
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className="mx-auto w-full max-w-3xl flex-1 px-4 py-10">{children}</main>
|
|
||||||
|
<main className="mx-auto w-full max-w-5xl flex-1 px-5 py-10">{children}</main>
|
||||||
|
|
||||||
<footer className="border-t border-[var(--border)]">
|
<footer className="border-t border-[var(--border)]">
|
||||||
<div className="mx-auto max-w-3xl px-4 py-6 text-sm italic text-[var(--muted)]">
|
<div className="mx-auto flex max-w-5xl flex-wrap items-center justify-between gap-2 px-5 py-6 text-sm text-[var(--muted)]">
|
||||||
where it came from matters
|
<span className="font-serif text-base italic">where it came from matters</span>
|
||||||
|
<span>Self-hosted · source-available · your data, your infrastructure</span>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+68
-18
@@ -1,27 +1,77 @@
|
|||||||
|
import { BadgeCheck, MapPin, ShieldCheck, Users } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
{
|
||||||
|
icon: Users,
|
||||||
|
title: "Family and land, together",
|
||||||
|
body: "People, relationships, and life events alongside property and chain-of-title — one documented story of where you come from.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: BadgeCheck,
|
||||||
|
title: "Sourced or it didn't happen",
|
||||||
|
body: "Every fact can carry a citation back to the record it came from. Sources are first-class, reusable, and visible.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: ShieldCheck,
|
||||||
|
title: "Yours to keep",
|
||||||
|
body: "Self-hosted and source-available. Living people protected by default. Open formats — export anytime, run it anywhere.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8 py-4">
|
<div className="space-y-20 py-6 sm:py-12">
|
||||||
<div className="space-y-4">
|
<section className="grid items-center gap-10 sm:grid-cols-[1.3fr_1fr]">
|
||||||
<h1 className="text-4xl font-semibold tracking-tight sm:text-5xl">
|
<div>
|
||||||
Where it came from matters
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-bronze">
|
||||||
</h1>
|
Family · Land · Provenance
|
||||||
<p className="max-w-prose text-lg text-[var(--muted)]">
|
</p>
|
||||||
Trace where you come from — your family <span className="text-bronze">and</span> your
|
<h1 className="mt-4 text-5xl font-semibold leading-[1.04] tracking-tight sm:text-6xl">
|
||||||
land — with every fact linked to a source, on infrastructure you control.
|
Where it came from{" "}
|
||||||
</p>
|
<span className="italic text-bronze">matters</span>.
|
||||||
</div>
|
</h1>
|
||||||
<div className="flex flex-wrap gap-3">
|
<p className="mt-6 max-w-xl text-lg leading-relaxed text-[var(--muted)]">
|
||||||
<Link href="/register">
|
Trace your family and your land in one place — every name, every parcel, every claim
|
||||||
<Button>Create an account</Button>
|
linked to the record it came from. Self-hosted, sourced, and yours to keep.
|
||||||
</Link>
|
</p>
|
||||||
<Link href="/login">
|
<div className="mt-8 flex flex-wrap gap-3">
|
||||||
<Button variant="outline">Sign in</Button>
|
<Link href="/register">
|
||||||
</Link>
|
<Button size="lg">Create your account</Button>
|
||||||
</div>
|
</Link>
|
||||||
|
<Link href="/login">
|
||||||
|
<Button size="lg" variant="outline">
|
||||||
|
Sign in
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden justify-self-end sm:block">
|
||||||
|
<div className="relative grid h-64 w-64 place-items-center rounded-full border border-[var(--border)] bg-[var(--surface)] shadow-[0_24px_60px_-24px_rgba(160,106,66,0.35)]">
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img src="/provenance-mark.svg" alt="" className="h-36 w-36" />
|
||||||
|
<MapPin className="absolute -right-2 top-10 h-7 w-7 text-bronze" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="grid gap-5 sm:grid-cols-3">
|
||||||
|
{features.map((f) => (
|
||||||
|
<div
|
||||||
|
key={f.title}
|
||||||
|
className="rounded-xl border border-[var(--border)] bg-[var(--surface)] p-6 shadow-[0_1px_2px_rgba(26,26,23,0.04)]"
|
||||||
|
>
|
||||||
|
<div className="grid h-10 w-10 place-items-center rounded-lg bg-bronze/12 text-bronze">
|
||||||
|
<f.icon className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<h2 className="mt-4 text-lg font-semibold">{f.title}</h2>
|
||||||
|
<p className="mt-2 text-sm leading-relaxed text-[var(--muted)]">{f.body}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,9 +56,14 @@ export default function TreeDetailPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Link href="/trees" className="text-sm text-[var(--muted)] hover:underline">
|
<div className="flex items-center justify-between">
|
||||||
← All trees
|
<Link href="/trees" className="text-sm text-[var(--muted)] hover:underline">
|
||||||
</Link>
|
← All trees
|
||||||
|
</Link>
|
||||||
|
<Link href={`/trees/${treeId}/sources`} className="text-sm text-bronze hover:underline">
|
||||||
|
Sources →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
@@ -15,10 +15,11 @@ type Event = components["schemas"]["EventRead"];
|
|||||||
type Relationship = components["schemas"]["RelationshipRead"];
|
type Relationship = components["schemas"]["RelationshipRead"];
|
||||||
type Qualifier = components["schemas"]["ParentChildQualifier"];
|
type Qualifier = components["schemas"]["ParentChildQualifier"];
|
||||||
type RelCreate = components["schemas"]["RelationshipCreate"];
|
type RelCreate = components["schemas"]["RelationshipCreate"];
|
||||||
|
type Source = components["schemas"]["SourceRead"];
|
||||||
|
type Citation = components["schemas"]["CitationRead"];
|
||||||
|
type CitationCreate = components["schemas"]["CitationCreate"];
|
||||||
|
|
||||||
const fieldCls =
|
const fieldCls = "h-9 rounded-md border border-[var(--border)] bg-[var(--surface)] px-2 text-sm";
|
||||||
"h-10 rounded-md border border-[var(--border)] bg-[var(--surface)] px-2 text-sm";
|
|
||||||
|
|
||||||
const QUALIFIERS: Qualifier[] = ["biological", "adoptive", "step", "foster", "donor", "guardian"];
|
const QUALIFIERS: Qualifier[] = ["biological", "adoptive", "step", "foster", "donor", "guardian"];
|
||||||
|
|
||||||
export default function PersonDetailPage() {
|
export default function PersonDetailPage() {
|
||||||
@@ -31,6 +32,8 @@ export default function PersonDetailPage() {
|
|||||||
const [people, setPeople] = useState<Person[]>([]);
|
const [people, setPeople] = useState<Person[]>([]);
|
||||||
const [events, setEvents] = useState<Event[]>([]);
|
const [events, setEvents] = useState<Event[]>([]);
|
||||||
const [rels, setRels] = useState<Relationship[]>([]);
|
const [rels, setRels] = useState<Relationship[]>([]);
|
||||||
|
const [sources, setSources] = useState<Source[]>([]);
|
||||||
|
const [citations, setCitations] = useState<Citation[]>([]);
|
||||||
const [ready, setReady] = useState(false);
|
const [ready, setReady] = useState(false);
|
||||||
|
|
||||||
const [evType, setEvType] = useState("birth");
|
const [evType, setEvType] = useState("birth");
|
||||||
@@ -40,6 +43,11 @@ export default function PersonDetailPage() {
|
|||||||
const [relOther, setRelOther] = useState("");
|
const [relOther, setRelOther] = useState("");
|
||||||
const [relQual, setRelQual] = useState<Qualifier>("biological");
|
const [relQual, setRelQual] = useState<Qualifier>("biological");
|
||||||
|
|
||||||
|
// Inline citation form: which fact is being cited ("p" = person, `e:<id>`).
|
||||||
|
const [citeFor, setCiteFor] = useState<string | null>(null);
|
||||||
|
const [citeSource, setCiteSource] = useState("");
|
||||||
|
const [citePage, setCitePage] = useState("");
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
const p = await api.GET("/api/v1/trees/{tree_id}/persons/{person_id}", {
|
const p = await api.GET("/api/v1/trees/{tree_id}/persons/{person_id}", {
|
||||||
params: { path: { tree_id: treeId, person_id: personId } },
|
params: { path: { tree_id: treeId, person_id: personId } },
|
||||||
@@ -49,7 +57,7 @@ export default function PersonDetailPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setPerson(p.data ?? null);
|
setPerson(p.data ?? null);
|
||||||
const [all, ev, rl] = await Promise.all([
|
const [all, ev, rl, src, cit] = await Promise.all([
|
||||||
api.GET("/api/v1/trees/{tree_id}/persons", { params: { path: { tree_id: treeId } } }),
|
api.GET("/api/v1/trees/{tree_id}/persons", { params: { path: { tree_id: treeId } } }),
|
||||||
api.GET("/api/v1/trees/{tree_id}/persons/{person_id}/events", {
|
api.GET("/api/v1/trees/{tree_id}/persons/{person_id}/events", {
|
||||||
params: { path: { tree_id: treeId, person_id: personId } },
|
params: { path: { tree_id: treeId, person_id: personId } },
|
||||||
@@ -57,10 +65,14 @@ export default function PersonDetailPage() {
|
|||||||
api.GET("/api/v1/trees/{tree_id}/persons/{person_id}/relationships", {
|
api.GET("/api/v1/trees/{tree_id}/persons/{person_id}/relationships", {
|
||||||
params: { path: { tree_id: treeId, person_id: personId } },
|
params: { path: { tree_id: treeId, person_id: personId } },
|
||||||
}),
|
}),
|
||||||
|
api.GET("/api/v1/trees/{tree_id}/sources", { params: { path: { tree_id: treeId } } }),
|
||||||
|
api.GET("/api/v1/trees/{tree_id}/citations", { params: { path: { tree_id: treeId } } }),
|
||||||
]);
|
]);
|
||||||
setPeople(all.data ?? []);
|
setPeople(all.data ?? []);
|
||||||
setEvents(ev.data ?? []);
|
setEvents(ev.data ?? []);
|
||||||
setRels(rl.data ?? []);
|
setRels(rl.data ?? []);
|
||||||
|
setSources(src.data ?? []);
|
||||||
|
setCitations(cit.data ?? []);
|
||||||
setReady(true);
|
setReady(true);
|
||||||
}, [router, treeId, personId]);
|
}, [router, treeId, personId]);
|
||||||
|
|
||||||
@@ -72,12 +84,18 @@ export default function PersonDetailPage() {
|
|||||||
const m = new Map(people.map((p) => [p.id, p.primary_name ?? "Unnamed"]));
|
const m = new Map(people.map((p) => [p.id, p.primary_name ?? "Unnamed"]));
|
||||||
return (id: string) => m.get(id) ?? "Unknown";
|
return (id: string) => m.get(id) ?? "Unknown";
|
||||||
}, [people]);
|
}, [people]);
|
||||||
|
const sourceName = useMemo(() => {
|
||||||
|
const m = new Map(sources.map((s) => [s.id, s.title]));
|
||||||
|
return (id: string) => m.get(id) ?? "source";
|
||||||
|
}, [sources]);
|
||||||
|
|
||||||
const others = people.filter((p) => p.id !== personId);
|
const others = people.filter((p) => p.id !== personId);
|
||||||
const parents = rels.filter((r) => r.type === "parent_child" && r.person_to_id === personId);
|
const parents = rels.filter((r) => r.type === "parent_child" && r.person_to_id === personId);
|
||||||
const children = rels.filter((r) => r.type === "parent_child" && r.person_from_id === personId);
|
const children = rels.filter((r) => r.type === "parent_child" && r.person_from_id === personId);
|
||||||
const partners = rels.filter((r) => r.type === "partnership");
|
const partners = rels.filter((r) => r.type === "partnership");
|
||||||
const siblings = rels.filter((r) => r.type === "sibling");
|
const siblings = rels.filter((r) => r.type === "sibling");
|
||||||
|
const eventCites = (id: string) => citations.filter((c) => c.event_id === id);
|
||||||
|
const personCites = citations.filter((c) => c.person_id === personId);
|
||||||
|
|
||||||
async function addEvent(e: React.FormEvent) {
|
async function addEvent(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -91,7 +109,6 @@ export default function PersonDetailPage() {
|
|||||||
load();
|
load();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeEvent(id: string) {
|
async function removeEvent(id: string) {
|
||||||
await api.DELETE("/api/v1/trees/{tree_id}/events/{event_id}", {
|
await api.DELETE("/api/v1/trees/{tree_id}/events/{event_id}", {
|
||||||
params: { path: { tree_id: treeId, event_id: id } },
|
params: { path: { tree_id: treeId, event_id: id } },
|
||||||
@@ -121,7 +138,6 @@ export default function PersonDetailPage() {
|
|||||||
load();
|
load();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeRel(id: string) {
|
async function removeRel(id: string) {
|
||||||
await api.DELETE("/api/v1/trees/{tree_id}/relationships/{relationship_id}", {
|
await api.DELETE("/api/v1/trees/{tree_id}/relationships/{relationship_id}", {
|
||||||
params: { path: { tree_id: treeId, relationship_id: id } },
|
params: { path: { tree_id: treeId, relationship_id: id } },
|
||||||
@@ -129,9 +145,100 @@ export default function PersonDetailPage() {
|
|||||||
load();
|
load();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function addCitation(target: Partial<CitationCreate>) {
|
||||||
|
if (!citeSource) return;
|
||||||
|
const body: CitationCreate = { source_id: citeSource, page: citePage || null, ...target };
|
||||||
|
const { error } = await api.POST("/api/v1/trees/{tree_id}/citations", {
|
||||||
|
params: { path: { tree_id: treeId } },
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
if (!error) {
|
||||||
|
setCiteFor(null);
|
||||||
|
setCiteSource("");
|
||||||
|
setCitePage("");
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function removeCitation(id: string) {
|
||||||
|
await api.DELETE("/api/v1/trees/{tree_id}/citations/{citation_id}", {
|
||||||
|
params: { path: { tree_id: treeId, citation_id: id } },
|
||||||
|
});
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
if (!ready) return <p className="text-[var(--muted)]">Loading…</p>;
|
if (!ready) return <p className="text-[var(--muted)]">Loading…</p>;
|
||||||
if (!person) return <p className="text-[var(--muted)]">Not found.</p>;
|
if (!person) return <p className="text-[var(--muted)]">Not found.</p>;
|
||||||
|
|
||||||
|
// Inline "cite" control: a badge with count, a toggle, and the picker form.
|
||||||
|
function citeControl(key: string, target: Partial<CitationCreate>, cites: Citation[]) {
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center gap-2">
|
||||||
|
{cites.length > 0 && (
|
||||||
|
<span
|
||||||
|
className="rounded bg-bronze/15 px-1.5 py-0.5 text-xs text-bronze"
|
||||||
|
title={cites.map((c) => sourceName(c.source_id)).join(", ")}
|
||||||
|
>
|
||||||
|
✓ {cites.length} sourced
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{citeFor === key ? (
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
addCitation(target);
|
||||||
|
}}
|
||||||
|
className="inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
className={fieldCls}
|
||||||
|
value={citeSource}
|
||||||
|
onChange={(e) => setCiteSource(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">— source —</option>
|
||||||
|
{sources.map((s) => (
|
||||||
|
<option key={s.id} value={s.id}>
|
||||||
|
{s.title}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
className={`${fieldCls} w-24`}
|
||||||
|
placeholder="page"
|
||||||
|
value={citePage}
|
||||||
|
onChange={(e) => setCitePage(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button type="submit" size="sm">
|
||||||
|
cite
|
||||||
|
</Button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCiteFor(null)}
|
||||||
|
className="text-xs text-[var(--muted)]"
|
||||||
|
>
|
||||||
|
cancel
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
) : sources.length === 0 ? (
|
||||||
|
<Link href={`/trees/${treeId}/sources`} className="text-xs text-[var(--muted)] hover:underline">
|
||||||
|
+ add a source first
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setCiteFor(key);
|
||||||
|
setCiteSource("");
|
||||||
|
setCitePage("");
|
||||||
|
}}
|
||||||
|
className="text-xs text-bronze hover:underline"
|
||||||
|
>
|
||||||
|
+ cite
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const relGroup = (label: string, items: Relationship[], otherId: (r: Relationship) => string) =>
|
const relGroup = (label: string, items: Relationship[], otherId: (r: Relationship) => string) =>
|
||||||
items.length > 0 && (
|
items.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
@@ -162,7 +269,10 @@ export default function PersonDetailPage() {
|
|||||||
← Back to tree
|
← Back to tree
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<h1 className="text-3xl font-semibold">{person.primary_name ?? "Unnamed person"}</h1>
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<h1 className="text-3xl font-semibold">{person.primary_name ?? "Unnamed person"}</h1>
|
||||||
|
{citeControl("p", { person_id: personId }, personCites)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -172,39 +282,32 @@ export default function PersonDetailPage() {
|
|||||||
{events.length === 0 ? (
|
{events.length === 0 ? (
|
||||||
<p className="text-sm text-[var(--muted)]">No events yet.</p>
|
<p className="text-sm text-[var(--muted)]">No events yet.</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="space-y-1">
|
<ul className="space-y-2">
|
||||||
{events.map((ev) => (
|
{events.map((ev) => (
|
||||||
<li key={ev.id} className="flex items-center justify-between text-sm">
|
<li key={ev.id} className="flex flex-wrap items-center justify-between gap-2 text-sm">
|
||||||
<span>
|
<span>
|
||||||
<span className="font-medium capitalize">{ev.event_type}</span>
|
<span className="font-medium capitalize">{ev.event_type}</span>
|
||||||
{ev.date_value ? (
|
{ev.date_value ? (
|
||||||
<span className="text-[var(--muted)]"> — {ev.date_value}</span>
|
<span className="text-[var(--muted)]"> — {ev.date_value}</span>
|
||||||
) : null}
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<span className="flex items-center gap-3">
|
||||||
onClick={() => removeEvent(ev.id)}
|
{citeControl(`e:${ev.id}`, { event_id: ev.id }, eventCites(ev.id))}
|
||||||
className="text-[var(--muted)] hover:text-bronze"
|
<button
|
||||||
aria-label="Remove"
|
onClick={() => removeEvent(ev.id)}
|
||||||
>
|
className="text-[var(--muted)] hover:text-bronze"
|
||||||
×
|
aria-label="Remove"
|
||||||
</button>
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
<form onSubmit={addEvent} className="flex flex-wrap gap-2">
|
<form onSubmit={addEvent} className="flex flex-wrap gap-2">
|
||||||
<Input
|
<Input className="w-36" placeholder="Event type" value={evType} onChange={(e) => setEvType(e.target.value)} />
|
||||||
className="w-36"
|
<Input className="w-40" placeholder="Date (e.g. ABT 1850)" value={evDate} onChange={(e) => setEvDate(e.target.value)} />
|
||||||
placeholder="Event type"
|
|
||||||
value={evType}
|
|
||||||
onChange={(e) => setEvType(e.target.value)}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
className="w-40"
|
|
||||||
placeholder="Date (e.g. ABT 1850)"
|
|
||||||
value={evDate}
|
|
||||||
onChange={(e) => setEvDate(e.target.value)}
|
|
||||||
/>
|
|
||||||
<Button type="submit">Add event</Button>
|
<Button type="submit">Add event</Button>
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -235,21 +338,13 @@ export default function PersonDetailPage() {
|
|||||||
) : (
|
) : (
|
||||||
<form onSubmit={addRel} className="flex flex-wrap items-center gap-2">
|
<form onSubmit={addRel} className="flex flex-wrap items-center gap-2">
|
||||||
<span className="text-sm text-[var(--muted)]">Add</span>
|
<span className="text-sm text-[var(--muted)]">Add</span>
|
||||||
<select
|
<select className={fieldCls} value={relKind} onChange={(e) => setRelKind(e.target.value as typeof relKind)}>
|
||||||
className={fieldCls}
|
|
||||||
value={relKind}
|
|
||||||
onChange={(e) => setRelKind(e.target.value as typeof relKind)}
|
|
||||||
>
|
|
||||||
<option value="parent">parent</option>
|
<option value="parent">parent</option>
|
||||||
<option value="child">child</option>
|
<option value="child">child</option>
|
||||||
<option value="partner">partner</option>
|
<option value="partner">partner</option>
|
||||||
<option value="sibling">sibling</option>
|
<option value="sibling">sibling</option>
|
||||||
</select>
|
</select>
|
||||||
<select
|
<select className={fieldCls} value={relOther} onChange={(e) => setRelOther(e.target.value)}>
|
||||||
className={fieldCls}
|
|
||||||
value={relOther}
|
|
||||||
onChange={(e) => setRelOther(e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="">— person —</option>
|
<option value="">— person —</option>
|
||||||
{others.map((p) => (
|
{others.map((p) => (
|
||||||
<option key={p.id} value={p.id}>
|
<option key={p.id} value={p.id}>
|
||||||
@@ -258,11 +353,7 @@ export default function PersonDetailPage() {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
{(relKind === "parent" || relKind === "child") && (
|
{(relKind === "parent" || relKind === "child") && (
|
||||||
<select
|
<select className={fieldCls} value={relQual} onChange={(e) => setRelQual(e.target.value as Qualifier)}>
|
||||||
className={fieldCls}
|
|
||||||
value={relQual}
|
|
||||||
onChange={(e) => setRelQual(e.target.value as Qualifier)}
|
|
||||||
>
|
|
||||||
{QUALIFIERS.map((q) => (
|
{QUALIFIERS.map((q) => (
|
||||||
<option key={q} value={q}>
|
<option key={q} value={q}>
|
||||||
{q}
|
{q}
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useParams, useRouter } from "next/navigation";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { api } from "@/lib/api/client";
|
||||||
|
import type { components } from "@/lib/api/schema";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
||||||
|
type Source = components["schemas"]["SourceRead"];
|
||||||
|
|
||||||
|
export default function SourcesPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams<{ id: string }>();
|
||||||
|
const treeId = params.id;
|
||||||
|
|
||||||
|
const [sources, setSources] = useState<Source[]>([]);
|
||||||
|
const [ready, setReady] = useState(false);
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [repository, setRepository] = useState("");
|
||||||
|
const [url, setUrl] = useState("");
|
||||||
|
|
||||||
|
const load = useCallback(async () => {
|
||||||
|
const { data, response } = await api.GET("/api/v1/trees/{tree_id}/sources", {
|
||||||
|
params: { path: { tree_id: treeId } },
|
||||||
|
});
|
||||||
|
if (response.status === 401) {
|
||||||
|
router.push("/login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSources(data ?? []);
|
||||||
|
setReady(true);
|
||||||
|
}, [router, treeId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
load();
|
||||||
|
}, [load]);
|
||||||
|
|
||||||
|
async function add(e: React.FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!title.trim()) return;
|
||||||
|
const { error } = await api.POST("/api/v1/trees/{tree_id}/sources", {
|
||||||
|
params: { path: { tree_id: treeId } },
|
||||||
|
body: { title, repository: repository || null, url: url || null },
|
||||||
|
});
|
||||||
|
if (!error) {
|
||||||
|
setTitle("");
|
||||||
|
setRepository("");
|
||||||
|
setUrl("");
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(id: string) {
|
||||||
|
await api.DELETE("/api/v1/trees/{tree_id}/sources/{source_id}", {
|
||||||
|
params: { path: { tree_id: treeId, source_id: id } },
|
||||||
|
});
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ready) return <p className="text-[var(--muted)]">Loading…</p>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Link href={`/trees/${treeId}`} className="text-sm text-[var(--muted)] hover:underline">
|
||||||
|
← Back to tree
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-2xl font-bold">Sources</h1>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">New source</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form onSubmit={add} className="flex flex-wrap gap-2">
|
||||||
|
<Input
|
||||||
|
className="w-56"
|
||||||
|
placeholder="Title (e.g. 1880 US Census)"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
className="w-40"
|
||||||
|
placeholder="Repository"
|
||||||
|
value={repository}
|
||||||
|
onChange={(e) => setRepository(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
className="w-48"
|
||||||
|
placeholder="URL"
|
||||||
|
value={url}
|
||||||
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button type="submit">Add source</Button>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{sources.length === 0 ? (
|
||||||
|
<p className="text-[var(--muted)]">No sources yet — add one above, then cite it on facts.</p>
|
||||||
|
) : (
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{sources.map((s) => (
|
||||||
|
<li key={s.id}>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="flex items-start justify-between gap-3 p-4">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{s.title}</div>
|
||||||
|
<div className="text-sm text-[var(--muted)]">
|
||||||
|
{[s.repository, s.url].filter(Boolean).join(" · ")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => remove(s.id)}
|
||||||
|
className="text-[var(--muted)] hover:text-bronze"
|
||||||
|
aria-label="Remove"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,19 +4,19 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bronze focus-visible:ring-offset-1 disabled:pointer-events-none disabled:opacity-50",
|
"inline-flex items-center justify-center gap-2 rounded-lg font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bronze focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--background)] disabled:pointer-events-none disabled:opacity-50",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
// Bronze is the brand accent; paper reads cleanly on it.
|
default: "bg-bronze text-paper shadow-sm hover:bg-bronze-deep hover:shadow",
|
||||||
default: "bg-bronze text-paper hover:bg-bronze-deep",
|
|
||||||
outline:
|
outline:
|
||||||
"border border-bronze text-bronze bg-transparent hover:bg-bronze hover:text-paper",
|
"border border-[var(--border)] bg-[var(--surface)] hover:border-bronze hover:text-bronze",
|
||||||
ghost: "text-[var(--foreground)] hover:bg-bronze/10",
|
ghost: "text-[var(--foreground)] hover:bg-bronze/10",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-10 px-4 py-2",
|
default: "h-10 px-4 text-sm",
|
||||||
sm: "h-9 px-3",
|
sm: "h-9 px-3 text-sm",
|
||||||
|
lg: "h-12 px-6 text-base",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: { variant: "default", size: "default" },
|
defaultVariants: { variant: "default", size: "default" },
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElemen
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-lg border border-[var(--border)] bg-[var(--surface)] shadow-sm",
|
"rounded-xl border border-[var(--border)] bg-[var(--surface)] shadow-[0_1px_2px_rgba(26,26,23,0.04),0_8px_24px_-12px_rgba(26,26,23,0.10)]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttribute
|
|||||||
<input
|
<input
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-10 w-full rounded-md border border-[var(--border)] bg-[var(--surface)] px-3 py-2 text-sm placeholder:text-[var(--muted)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bronze disabled:opacity-50",
|
"flex h-10 w-full rounded-lg border border-[var(--border)] bg-[var(--surface)] px-3 py-2 text-sm placeholder:text-[var(--muted)] transition-colors focus-visible:border-bronze focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bronze/40 disabled:opacity-50",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
Vendored
+410
@@ -329,10 +329,143 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/api/v1/trees/{tree_id}/sources": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
/** List Sources */
|
||||||
|
get: operations["list_sources_api_v1_trees__tree_id__sources_get"];
|
||||||
|
put?: never;
|
||||||
|
/** Create Source */
|
||||||
|
post: operations["create_source_api_v1_trees__tree_id__sources_post"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
"/api/v1/trees/{tree_id}/sources/{source_id}": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
/** Get Source */
|
||||||
|
get: operations["get_source_api_v1_trees__tree_id__sources__source_id__get"];
|
||||||
|
put?: never;
|
||||||
|
post?: never;
|
||||||
|
/** Delete Source */
|
||||||
|
delete: operations["delete_source_api_v1_trees__tree_id__sources__source_id__delete"];
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
"/api/v1/trees/{tree_id}/citations": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
/** List Citations */
|
||||||
|
get: operations["list_citations_api_v1_trees__tree_id__citations_get"];
|
||||||
|
put?: never;
|
||||||
|
/** Create Citation */
|
||||||
|
post: operations["create_citation_api_v1_trees__tree_id__citations_post"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
"/api/v1/trees/{tree_id}/citations/{citation_id}": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
post?: never;
|
||||||
|
/** Delete Citation */
|
||||||
|
delete: operations["delete_citation_api_v1_trees__tree_id__citations__citation_id__delete"];
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
export type webhooks = Record<string, never>;
|
export type webhooks = Record<string, never>;
|
||||||
export interface components {
|
export interface components {
|
||||||
schemas: {
|
schemas: {
|
||||||
|
/**
|
||||||
|
* CitationConfidence
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
CitationConfidence: "high" | "medium" | "low";
|
||||||
|
/** CitationCreate */
|
||||||
|
CitationCreate: {
|
||||||
|
/**
|
||||||
|
* Source Id
|
||||||
|
* Format: uuid
|
||||||
|
*/
|
||||||
|
source_id: string;
|
||||||
|
/** Person Id */
|
||||||
|
person_id?: string | null;
|
||||||
|
/** Event Id */
|
||||||
|
event_id?: string | null;
|
||||||
|
/** Name Id */
|
||||||
|
name_id?: string | null;
|
||||||
|
/** Relationship Id */
|
||||||
|
relationship_id?: string | null;
|
||||||
|
/** Page */
|
||||||
|
page?: string | null;
|
||||||
|
/** Detail */
|
||||||
|
detail?: string | null;
|
||||||
|
confidence?: components["schemas"]["CitationConfidence"] | null;
|
||||||
|
};
|
||||||
|
/** CitationRead */
|
||||||
|
CitationRead: {
|
||||||
|
/**
|
||||||
|
* Id
|
||||||
|
* Format: uuid
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* Tree Id
|
||||||
|
* Format: uuid
|
||||||
|
*/
|
||||||
|
tree_id: string;
|
||||||
|
/**
|
||||||
|
* Source Id
|
||||||
|
* Format: uuid
|
||||||
|
*/
|
||||||
|
source_id: string;
|
||||||
|
/** Person Id */
|
||||||
|
person_id: string | null;
|
||||||
|
/** Event Id */
|
||||||
|
event_id: string | null;
|
||||||
|
/** Name Id */
|
||||||
|
name_id: string | null;
|
||||||
|
/** Relationship Id */
|
||||||
|
relationship_id: string | null;
|
||||||
|
/** Page */
|
||||||
|
page: string | null;
|
||||||
|
/** Detail */
|
||||||
|
detail: string | null;
|
||||||
|
confidence: components["schemas"]["CitationConfidence"] | null;
|
||||||
|
/**
|
||||||
|
* Created At
|
||||||
|
* Format: date-time
|
||||||
|
*/
|
||||||
|
created_at: string;
|
||||||
|
};
|
||||||
/** EventCreate */
|
/** EventCreate */
|
||||||
EventCreate: {
|
EventCreate: {
|
||||||
/** Event Type */
|
/** Event Type */
|
||||||
@@ -552,6 +685,59 @@ export interface components {
|
|||||||
*/
|
*/
|
||||||
expires_at: string;
|
expires_at: string;
|
||||||
};
|
};
|
||||||
|
/** SourceCreate */
|
||||||
|
SourceCreate: {
|
||||||
|
/** Title */
|
||||||
|
title: string;
|
||||||
|
/** Author */
|
||||||
|
author?: string | null;
|
||||||
|
/** Source Type */
|
||||||
|
source_type?: string | null;
|
||||||
|
/** Repository */
|
||||||
|
repository?: string | null;
|
||||||
|
/** Url */
|
||||||
|
url?: string | null;
|
||||||
|
/** Citation Text */
|
||||||
|
citation_text?: string | null;
|
||||||
|
/** Publication Info */
|
||||||
|
publication_info?: string | null;
|
||||||
|
/** Quality Note */
|
||||||
|
quality_note?: string | null;
|
||||||
|
};
|
||||||
|
/** SourceRead */
|
||||||
|
SourceRead: {
|
||||||
|
/**
|
||||||
|
* Id
|
||||||
|
* Format: uuid
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* Tree Id
|
||||||
|
* Format: uuid
|
||||||
|
*/
|
||||||
|
tree_id: string;
|
||||||
|
/** Title */
|
||||||
|
title: string;
|
||||||
|
/** Author */
|
||||||
|
author: string | null;
|
||||||
|
/** Source Type */
|
||||||
|
source_type: string | null;
|
||||||
|
/** Repository */
|
||||||
|
repository: string | null;
|
||||||
|
/** Url */
|
||||||
|
url: string | null;
|
||||||
|
/** Citation Text */
|
||||||
|
citation_text: string | null;
|
||||||
|
/** Publication Info */
|
||||||
|
publication_info: string | null;
|
||||||
|
/** Quality Note */
|
||||||
|
quality_note: string | null;
|
||||||
|
/**
|
||||||
|
* Created At
|
||||||
|
* Format: date-time
|
||||||
|
*/
|
||||||
|
created_at: string;
|
||||||
|
};
|
||||||
/** TokenRequest */
|
/** TokenRequest */
|
||||||
TokenRequest: {
|
TokenRequest: {
|
||||||
/** Token */
|
/** Token */
|
||||||
@@ -1256,4 +1442,228 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
list_sources_api_v1_trees__tree_id__sources_get: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
tree_id: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["SourceRead"][];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
create_source_api_v1_trees__tree_id__sources_post: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
tree_id: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["SourceCreate"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
201: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["SourceRead"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
get_source_api_v1_trees__tree_id__sources__source_id__get: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
tree_id: string;
|
||||||
|
source_id: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["SourceRead"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
delete_source_api_v1_trees__tree_id__sources__source_id__delete: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
tree_id: string;
|
||||||
|
source_id: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
204: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
list_citations_api_v1_trees__tree_id__citations_get: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
tree_id: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["CitationRead"][];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
create_citation_api_v1_trees__tree_id__citations_post: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
tree_id: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["CitationCreate"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
201: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["CitationRead"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
delete_citation_api_v1_trees__tree_id__citations__citation_id__delete: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
tree_id: string;
|
||||||
|
citation_id: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
204: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -849,10 +849,571 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/trees/{tree_id}/sources": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"sources"
|
||||||
|
],
|
||||||
|
"summary": "Create Source",
|
||||||
|
"operationId": "create_source_api_v1_trees__tree_id__sources_post",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "tree_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Tree Id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/SourceCreate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/SourceRead"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"sources"
|
||||||
|
],
|
||||||
|
"summary": "List Sources",
|
||||||
|
"operationId": "list_sources_api_v1_trees__tree_id__sources_get",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "tree_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Tree Id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/SourceRead"
|
||||||
|
},
|
||||||
|
"title": "Response List Sources Api V1 Trees Tree Id Sources Get"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/trees/{tree_id}/sources/{source_id}": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"sources"
|
||||||
|
],
|
||||||
|
"summary": "Get Source",
|
||||||
|
"operationId": "get_source_api_v1_trees__tree_id__sources__source_id__get",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "tree_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Tree Id"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "source_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Source Id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/SourceRead"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"sources"
|
||||||
|
],
|
||||||
|
"summary": "Delete Source",
|
||||||
|
"operationId": "delete_source_api_v1_trees__tree_id__sources__source_id__delete",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "tree_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Tree Id"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "source_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Source Id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "Successful Response"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/trees/{tree_id}/citations": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"citations"
|
||||||
|
],
|
||||||
|
"summary": "Create Citation",
|
||||||
|
"operationId": "create_citation_api_v1_trees__tree_id__citations_post",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "tree_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Tree Id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CitationCreate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CitationRead"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"citations"
|
||||||
|
],
|
||||||
|
"summary": "List Citations",
|
||||||
|
"operationId": "list_citations_api_v1_trees__tree_id__citations_get",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "tree_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Tree Id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/CitationRead"
|
||||||
|
},
|
||||||
|
"title": "Response List Citations Api V1 Trees Tree Id Citations Get"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/trees/{tree_id}/citations/{citation_id}": {
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"citations"
|
||||||
|
],
|
||||||
|
"summary": "Delete Citation",
|
||||||
|
"operationId": "delete_citation_api_v1_trees__tree_id__citations__citation_id__delete",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "tree_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Tree Id"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "citation_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Citation Id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "Successful Response"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
"schemas": {
|
"schemas": {
|
||||||
|
"CitationConfidence": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"high",
|
||||||
|
"medium",
|
||||||
|
"low"
|
||||||
|
],
|
||||||
|
"title": "CitationConfidence"
|
||||||
|
},
|
||||||
|
"CitationCreate": {
|
||||||
|
"properties": {
|
||||||
|
"source_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Source Id"
|
||||||
|
},
|
||||||
|
"person_id": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Person Id"
|
||||||
|
},
|
||||||
|
"event_id": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Event Id"
|
||||||
|
},
|
||||||
|
"name_id": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Name Id"
|
||||||
|
},
|
||||||
|
"relationship_id": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Relationship Id"
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Page"
|
||||||
|
},
|
||||||
|
"detail": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Detail"
|
||||||
|
},
|
||||||
|
"confidence": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/CitationConfidence"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"source_id"
|
||||||
|
],
|
||||||
|
"title": "CitationCreate"
|
||||||
|
},
|
||||||
|
"CitationRead": {
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Id"
|
||||||
|
},
|
||||||
|
"tree_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Tree Id"
|
||||||
|
},
|
||||||
|
"source_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Source Id"
|
||||||
|
},
|
||||||
|
"person_id": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Person Id"
|
||||||
|
},
|
||||||
|
"event_id": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Event Id"
|
||||||
|
},
|
||||||
|
"name_id": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Name Id"
|
||||||
|
},
|
||||||
|
"relationship_id": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Relationship Id"
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Page"
|
||||||
|
},
|
||||||
|
"detail": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Detail"
|
||||||
|
},
|
||||||
|
"confidence": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/CitationConfidence"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"title": "Created At"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"tree_id",
|
||||||
|
"source_id",
|
||||||
|
"person_id",
|
||||||
|
"event_id",
|
||||||
|
"name_id",
|
||||||
|
"relationship_id",
|
||||||
|
"page",
|
||||||
|
"detail",
|
||||||
|
"confidence",
|
||||||
|
"created_at"
|
||||||
|
],
|
||||||
|
"title": "CitationRead"
|
||||||
|
},
|
||||||
"EventCreate": {
|
"EventCreate": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"event_type": {
|
"event_type": {
|
||||||
@@ -1512,6 +2073,211 @@
|
|||||||
],
|
],
|
||||||
"title": "SessionRead"
|
"title": "SessionRead"
|
||||||
},
|
},
|
||||||
|
"SourceCreate": {
|
||||||
|
"properties": {
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Title"
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Author"
|
||||||
|
},
|
||||||
|
"source_type": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Source Type"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Repository"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Url"
|
||||||
|
},
|
||||||
|
"citation_text": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Citation Text"
|
||||||
|
},
|
||||||
|
"publication_info": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Publication Info"
|
||||||
|
},
|
||||||
|
"quality_note": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Quality Note"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"title"
|
||||||
|
],
|
||||||
|
"title": "SourceCreate"
|
||||||
|
},
|
||||||
|
"SourceRead": {
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Id"
|
||||||
|
},
|
||||||
|
"tree_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Tree Id"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Title"
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Author"
|
||||||
|
},
|
||||||
|
"source_type": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Source Type"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Repository"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Url"
|
||||||
|
},
|
||||||
|
"citation_text": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Citation Text"
|
||||||
|
},
|
||||||
|
"publication_info": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Publication Info"
|
||||||
|
},
|
||||||
|
"quality_note": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Quality Note"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"title": "Created At"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"tree_id",
|
||||||
|
"title",
|
||||||
|
"author",
|
||||||
|
"source_type",
|
||||||
|
"repository",
|
||||||
|
"url",
|
||||||
|
"citation_text",
|
||||||
|
"publication_info",
|
||||||
|
"quality_note",
|
||||||
|
"created_at"
|
||||||
|
],
|
||||||
|
"title": "SourceRead"
|
||||||
|
},
|
||||||
"TokenRequest": {
|
"TokenRequest": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"token": {
|
"token": {
|
||||||
|
|||||||
Reference in New Issue
Block a user