Alternate names (maiden/married), self-person link, deletion integrity

Names (the genealogy standard: maiden name primary, married/alias as typed
alternates):
- Name model already supported multiple typed names; expose full CRUD —
  NameCreate/Read/Update schemas, name_service (one-primary invariant,
  promote-on-delete), nested /persons/{id}/names routes.
- Person page gains a Names card: add/edit/delete + "make primary", with a
  curated name_type dropdown (birth/maiden, married, alias, nickname, …).

Self-person ("who am I"):
- users.self_person_id FK (use_alter for the users<->persons<->trees cycle)
  + migration; PATCH /users/me/self-person; "This is me" / "This is you"
  on the person page. Soft-deleting the linked person clears it.

Deletion integrity (fixes the broken tree view):
- delete_person now soft-deletes the relationships touching the person, so no
  dangling edges remain; family-chart also filters links to missing people.
- Optional cascade=true recursively deletes descendants (GEDCOM cleanup);
  the person page asks "only this person" vs "with all descendants".
- DELETE returns {deleted: n}.

Family view surfaces "Not connected to anyone" so dangling people aren't lost.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 10:21:12 -04:00
parent f165ccb941
commit 04ccdbf96a
19 changed files with 2004 additions and 33 deletions
+45
View File
@@ -317,6 +317,17 @@ export default function FamilyViewPage() {
const partners = partnersOf(focus.id);
const children = childrenOf(focus.id);
// "Dangling" people: not linked to anyone. Common after a GEDCOM import or a
// mistaken delete — surface them so they're not lost in the directory.
const connected = new Set<string>();
for (const r of rels) {
connected.add(r.person_from_id);
connected.add(r.person_to_id);
}
const unconnected = people
.filter((p) => !connected.has(p.id))
.sort((a, b) => (a.primary_name ?? "").localeCompare(b.primary_name ?? ""));
const sorted = [...people].sort((a, b) =>
(a.primary_name ?? "").localeCompare(b.primary_name ?? ""),
);
@@ -380,6 +391,40 @@ export default function FamilyViewPage() {
</Card>
</div>
{/* Unconnected people — not linked to anyone in the tree */}
{unconnected.length > 0 && (
<Card className="border-bronze/40">
<CardContent className="space-y-3 p-6">
<div className="flex items-center justify-between">
<h2 className="font-serif text-base font-semibold">
Not connected to anyone ({unconnected.length})
</h2>
<span className="text-xs text-[var(--muted)]">
Open one and add a relationship, or delete it.
</span>
</div>
<div className="flex flex-wrap gap-3">
{unconnected.slice(0, 60).map((p) => (
<div key={p.id} className="flex items-center gap-1">
<PersonBox id={p.id} muted />
<Link
href={`/trees/${treeId}/persons/${p.id}`}
className="text-xs text-bronze hover:underline"
>
open
</Link>
</div>
))}
</div>
{unconnected.length > 60 && (
<p className="text-xs text-[var(--muted)]">
Showing 60 of {unconnected.length}.
</p>
)}
</CardContent>
</Card>
)}
{/* Scrollable, searchable people directory (scales to large trees) */}
<div className="space-y-3">
<div className="flex flex-wrap items-center justify-between gap-3">
@@ -12,6 +12,8 @@ import { Input } from "@/components/ui/input";
import { PersonCombobox } from "@/components/person-combobox";
type Person = components["schemas"]["PersonRead"];
type Name = components["schemas"]["NameRead"];
type Me = components["schemas"]["UserRead"];
type Event = components["schemas"]["EventRead"];
type Relationship = components["schemas"]["RelationshipRead"];
type Qualifier = components["schemas"]["ParentChildQualifier"];
@@ -23,6 +25,21 @@ type CitationCreate = components["schemas"]["CitationCreate"];
const fieldCls = "h-9 rounded-md border border-[var(--border)] bg-[var(--surface)] px-2 text-sm";
const QUALIFIERS: Qualifier[] = ["biological", "adoptive", "step", "foster", "donor", "guardian"];
// Typed name vocabulary. "birth" is the maiden/birth name; "married" etc. are
// alternates. The maiden name stays primary by convention (Ancestry/FamilySearch).
const NAME_TYPES: { value: string; label: string }[] = [
{ value: "birth", label: "Birth / maiden" },
{ value: "married", label: "Married" },
{ value: "alias", label: "Also known as" },
{ value: "nickname", label: "Nickname" },
{ value: "religious", label: "Religious" },
{ value: "immigration", label: "Anglicized" },
];
const nameTypeLabel = (t: string) =>
NAME_TYPES.find((n) => n.value === t)?.label ?? t;
const formatName = (n: Name) =>
[n.given, n.surname].filter(Boolean).join(" ") || "—";
// Curated genealogical event vocabulary (with an escape hatch).
const EVENT_TYPES = [
"birth", "death", "marriage", "divorce", "engagement", "baptism", "burial",
@@ -89,6 +106,8 @@ export default function PersonDetailPage() {
const [person, setPerson] = useState<Person | null>(null);
const [people, setPeople] = useState<Person[]>([]);
const [names, setNames] = useState<Name[]>([]);
const [me, setMe] = useState<Me | null>(null);
const [events, setEvents] = useState<Event[]>([]);
const [rels, setRels] = useState<Relationship[]>([]);
const [sources, setSources] = useState<Source[]>([]);
@@ -123,6 +142,18 @@ export default function PersonDetailPage() {
const [relOther, setRelOther] = useState("");
const [relQual, setRelQual] = useState<Qualifier>("biological");
// Add-name form + inline edit.
const [nameType, setNameType] = useState("married");
const [nGiven, setNGiven] = useState("");
const [nSurname, setNSurname] = useState("");
const [editNameId, setEditNameId] = useState<string | null>(null);
const [enType, setEnType] = useState("married");
const [enGiven, setEnGiven] = useState("");
const [enSurname, setEnSurname] = useState("");
// Delete confirmation (with optional cascade to descendants).
const [confirmingDelete, setConfirmingDelete] = useState(false);
// Inline citation form: which fact is being cited ("p" = person, `e:<id>`).
const [citeFor, setCiteFor] = useState<string | null>(null);
const [citeSource, setCiteSource] = useState("");
@@ -137,8 +168,12 @@ export default function PersonDetailPage() {
return;
}
setPerson(p.data ?? null);
const [all, ev, rl, src, cit] = await Promise.all([
const [all, nm, mine, 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/{person_id}/names", {
params: { path: { tree_id: treeId, person_id: personId } },
}),
api.GET("/api/v1/users/me"),
api.GET("/api/v1/trees/{tree_id}/persons/{person_id}/events", {
params: { path: { tree_id: treeId, person_id: personId } },
}),
@@ -149,6 +184,8 @@ export default function PersonDetailPage() {
api.GET("/api/v1/trees/{tree_id}/citations", { params: { path: { tree_id: treeId } } }),
]);
setPeople(all.data ?? []);
setNames(nm.data ?? []);
setMe(mine.data ?? null);
setEvents(ev.data ?? []);
setRels(rl.data ?? []);
setSources(src.data ?? []);
@@ -284,13 +321,72 @@ export default function PersonDetailPage() {
load();
}
async function removePerson() {
async function removePerson(cascade: boolean) {
await api.DELETE("/api/v1/trees/{tree_id}/persons/{person_id}", {
params: { path: { tree_id: treeId, person_id: personId } },
params: { path: { tree_id: treeId, person_id: personId }, query: { cascade } },
});
router.push(`/trees/${treeId}`);
}
async function addName(e: React.FormEvent) {
e.preventDefault();
if (!nGiven.trim() && !nSurname.trim()) return;
const { error } = await api.POST("/api/v1/trees/{tree_id}/persons/{person_id}/names", {
params: { path: { tree_id: treeId, person_id: personId } },
body: { name_type: nameType, given: nGiven || null, surname: nSurname || null },
});
if (!error) {
setNGiven("");
setNSurname("");
setNameType("married");
load();
}
}
function startEditName(n: Name) {
setEditNameId(n.id);
setEnType(n.name_type);
setEnGiven(n.given ?? "");
setEnSurname(n.surname ?? "");
}
async function saveName() {
if (!editNameId) return;
const { error } = await api.PATCH(
"/api/v1/trees/{tree_id}/persons/{person_id}/names/{name_id}",
{
params: { path: { tree_id: treeId, person_id: personId, name_id: editNameId } },
body: { name_type: enType, given: enGiven || null, surname: enSurname || null },
},
);
if (!error) {
setEditNameId(null);
load();
}
}
async function makePrimaryName(id: string) {
await api.PATCH("/api/v1/trees/{tree_id}/persons/{person_id}/names/{name_id}", {
params: { path: { tree_id: treeId, person_id: personId, name_id: id } },
body: { is_primary: true },
});
load();
}
async function removeName(id: string) {
await api.DELETE("/api/v1/trees/{tree_id}/persons/{person_id}/names/{name_id}", {
params: { path: { tree_id: treeId, person_id: personId, name_id: id } },
});
load();
}
async function setSelf(link: boolean) {
await api.PATCH("/api/v1/users/me/self-person", {
body: { self_person_id: link ? personId : null },
});
load();
}
function startEditPerson(current: Person) {
const t = (current.primary_name ?? "").trim().split(/\s+/).filter(Boolean);
setPGiven(t.length > 1 ? t.slice(0, -1).join(" ") : (t[0] ?? ""));
@@ -321,6 +417,8 @@ export default function PersonDetailPage() {
if (!ready) return <p className="text-[var(--muted)]">Loading</p>;
if (!person) return <p className="text-[var(--muted)]">Not found.</p>;
const isSelf = me?.self_person_id === personId;
// Inline "cite" control: a badge with count, a toggle, and the picker form.
function citeControl(key: string, target: Partial<CitationCreate>, cites: Citation[]) {
return (
@@ -463,19 +561,195 @@ export default function PersonDetailPage() {
</form>
) : (
<div className="flex flex-wrap items-center justify-between gap-2">
<h1 className="text-3xl font-semibold">{person.primary_name ?? "Unnamed person"}</h1>
<h1 className="flex items-center gap-3 text-3xl font-semibold">
{person.primary_name ?? "Unnamed person"}
{isSelf && (
<span className="rounded-full bg-bronze/15 px-2.5 py-1 text-xs font-medium text-bronze">
This is you
</span>
)}
</h1>
<div className="flex items-center gap-3">
{citeControl("p", { person_id: personId }, personCites)}
{isSelf ? (
<Button variant="ghost" size="sm" onClick={() => setSelf(false)}>
Unlink me
</Button>
) : (
<Button variant="outline" size="sm" onClick={() => setSelf(true)}>
This is me
</Button>
)}
<Button variant="outline" size="sm" onClick={() => startEditPerson(person)}>
Edit
</Button>
<Button variant="ghost" size="sm" onClick={removePerson}>
<Button variant="ghost" size="sm" onClick={() => setConfirmingDelete(true)}>
Delete
</Button>
</div>
</div>
)}
{confirmingDelete && (
<div className="space-y-3 rounded-lg border border-bronze/40 bg-bronze/[0.05] p-4">
<p className="text-sm">
Delete <strong>{person.primary_name ?? "this person"}</strong>? Their relationships
will be removed too. This can be undone from Recovery.
</p>
<div className="flex flex-wrap gap-2">
<Button variant="ghost" size="sm" onClick={() => removePerson(false)}>
Delete only this person
</Button>
<Button variant="outline" size="sm" onClick={() => removePerson(true)}>
Delete with all descendants
</Button>
<button
type="button"
onClick={() => setConfirmingDelete(false)}
className="text-xs text-[var(--muted)]"
>
cancel
</button>
</div>
</div>
)}
<Card>
<CardHeader>
<CardTitle className="text-base">Names</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{names.length === 0 ? (
<p className="text-sm text-[var(--muted)]">No names yet.</p>
) : (
<ul className="space-y-2">
{names.map((n) =>
editNameId === n.id ? (
<li key={n.id}>
<form
onSubmit={(e) => {
e.preventDefault();
saveName();
}}
className="flex flex-wrap items-center gap-2"
>
<select
className={fieldCls}
value={enType}
onChange={(e) => setEnType(e.target.value)}
>
{NAME_TYPES.map((t) => (
<option key={t.value} value={t.value}>
{t.label}
</option>
))}
</select>
<Input
className="h-9 w-36"
placeholder="Given"
value={enGiven}
onChange={(e) => setEnGiven(e.target.value)}
/>
<Input
className="h-9 w-36"
placeholder="Surname"
value={enSurname}
onChange={(e) => setEnSurname(e.target.value)}
/>
<Button type="submit" size="sm">
Save
</Button>
<button
type="button"
onClick={() => setEditNameId(null)}
className="text-xs text-[var(--muted)]"
>
cancel
</button>
</form>
</li>
) : (
<li
key={n.id}
className="flex flex-wrap items-center justify-between gap-2 text-sm"
>
<span className="flex items-center gap-2">
<span className="font-medium">{formatName(n)}</span>
<span className="rounded bg-[var(--border)]/50 px-1.5 py-0.5 text-xs text-[var(--muted)]">
{nameTypeLabel(n.name_type)}
</span>
{n.is_primary && (
<span className="rounded bg-bronze/15 px-1.5 py-0.5 text-xs text-bronze">
primary
</span>
)}
</span>
<span className="flex items-center gap-3">
{!n.is_primary && (
<button
onClick={() => makePrimaryName(n.id)}
className="text-xs text-bronze hover:underline"
>
make primary
</button>
)}
<button
onClick={() => startEditName(n)}
className="text-xs text-bronze hover:underline"
>
edit
</button>
<button
onClick={() => removeName(n.id)}
className="text-[var(--muted)] hover:text-bronze"
aria-label="Remove"
>
×
</button>
</span>
</li>
),
)}
</ul>
)}
<form onSubmit={addName} className="flex flex-wrap items-end gap-2">
<label className="flex flex-col gap-1">
<span className="text-xs text-[var(--muted)]">Type</span>
<select
className={fieldCls}
value={nameType}
onChange={(e) => setNameType(e.target.value)}
>
{NAME_TYPES.map((t) => (
<option key={t.value} value={t.value}>
{t.label}
</option>
))}
</select>
</label>
<label className="flex flex-col gap-1">
<span className="text-xs text-[var(--muted)]">Given</span>
<Input
className="h-9 w-36"
placeholder="Given"
value={nGiven}
onChange={(e) => setNGiven(e.target.value)}
/>
</label>
<label className="flex flex-col gap-1">
<span className="text-xs text-[var(--muted)]">Surname</span>
<Input
className="h-9 w-36"
placeholder="Surname"
value={nSurname}
onChange={(e) => setNSurname(e.target.value)}
/>
</label>
<Button type="submit">Add name</Button>
</form>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">Life events</CardTitle>
+10 -1
View File
@@ -104,6 +104,11 @@ export default function TreePage() {
if (status !== "ready" || mode === "fan" || !containerRef.current) return;
let cancelled = false;
(async () => {
// Only link to people that still exist — a soft-deleted person leaves
// dangling relationship rows, and family-chart breaks on an id with no
// matching datum. Filter them out so a deletion never blanks the tree.
const alive = new Set(people.map((pp) => pp.id));
const keep = (ids: string[]) => ids.filter((id) => alive.has(id));
const data = people.map((pp) => {
const [fn, ln] = splitName(pp.primary_name);
return {
@@ -114,7 +119,11 @@ export default function TreePage() {
birthday: years.get(pp.id) ?? "",
gender: pp.gender === "female" ? "F" : "M",
},
rels: { spouses: partnersOf(pp.id), parents: parentsOf(pp.id), children: childrenOf(pp.id) },
rels: {
spouses: keep(partnersOf(pp.id)),
parents: keep(parentsOf(pp.id)),
children: keep(childrenOf(pp.id)),
},
};
});
const f3 = await import("family-chart");
+325 -4
View File
@@ -157,6 +157,26 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/v1/users/me/self-person": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
/**
* Set Self Person
* @description Link (or unlink) the Person record that represents this account.
*/
patch: operations["set_self_person_api_v1_users_me_self_person_patch"];
trace?: never;
};
"/api/v1/trees": {
parameters: {
query?: never;
@@ -240,7 +260,11 @@ export interface paths {
get: operations["get_person_api_v1_trees__tree_id__persons__person_id__get"];
put?: never;
post?: never;
/** Delete Person */
/**
* Delete Person
* @description Delete a person. ``cascade=true`` also deletes all descendants. Returns
* the number of persons deleted (1 unless cascading).
*/
delete: operations["delete_person_api_v1_trees__tree_id__persons__person_id__delete"];
options?: never;
head?: never;
@@ -265,6 +289,42 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/v1/trees/{tree_id}/persons/{person_id}/names": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** List Names */
get: operations["list_names_api_v1_trees__tree_id__persons__person_id__names_get"];
put?: never;
/** Create Name */
post: operations["create_name_api_v1_trees__tree_id__persons__person_id__names_post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/trees/{tree_id}/persons/{person_id}/names/{name_id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
/** Delete Name */
delete: operations["delete_name_api_v1_trees__tree_id__persons__person_id__names__name_id__delete"];
options?: never;
head?: never;
/** Update Name */
patch: operations["update_name_api_v1_trees__tree_id__persons__person_id__names__name_id__patch"];
trace?: never;
};
"/api/v1/trees/{tree_id}/events": {
parameters: {
query?: never;
@@ -780,6 +840,85 @@ export interface components {
/** Source Id */
source_id?: string | null;
};
/** NameCreate */
NameCreate: {
/**
* Name Type
* @default birth
*/
name_type?: string;
/** Given */
given?: string | null;
/** Surname */
surname?: string | null;
/** Prefix */
prefix?: string | null;
/** Suffix */
suffix?: string | null;
/** Nickname */
nickname?: string | null;
/**
* Is Primary
* @default false
*/
is_primary?: boolean;
};
/** NameRead */
NameRead: {
/**
* Id
* Format: uuid
*/
id: string;
/**
* Tree Id
* Format: uuid
*/
tree_id: string;
/**
* Person Id
* Format: uuid
*/
person_id: string;
/** Name Type */
name_type: string;
/** Given */
given: string | null;
/** Surname */
surname: string | null;
/** Prefix */
prefix: string | null;
/** Suffix */
suffix: string | null;
/** Nickname */
nickname: string | null;
/** Is Primary */
is_primary: boolean;
/** Sort Order */
sort_order: number;
/**
* Created At
* Format: date-time
*/
created_at: string;
};
/** NameUpdate */
NameUpdate: {
/** Name Type */
name_type?: string | null;
/** Given */
given?: string | null;
/** Surname */
surname?: string | null;
/** Prefix */
prefix?: string | null;
/** Suffix */
suffix?: string | null;
/** Nickname */
nickname?: string | null;
/** Is Primary */
is_primary?: boolean | null;
};
/**
* ParentChildQualifier
* @description Qualifies a parent_child edge so adoption/donor/blended families are
@@ -1074,12 +1213,19 @@ export interface components {
display_name: string | null;
/** Email Verified At */
email_verified_at: string | null;
/** Self Person Id */
self_person_id?: string | null;
/**
* Created At
* Format: date-time
*/
created_at: string;
};
/** UserSelfPersonUpdate */
UserSelfPersonUpdate: {
/** Self Person Id */
self_person_id?: string | null;
};
/** ValidationError */
ValidationError: {
/** Location */
@@ -1347,6 +1493,39 @@ export interface operations {
};
};
};
set_self_person_api_v1_users_me_self_person_patch: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["UserSelfPersonUpdate"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["UserRead"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
list_my_trees_api_v1_trees_get: {
parameters: {
query?: {
@@ -1640,7 +1819,9 @@ export interface operations {
};
delete_person_api_v1_trees__tree_id__persons__person_id__delete: {
parameters: {
query?: never;
query?: {
cascade?: boolean;
};
header?: never;
path: {
tree_id: string;
@@ -1651,11 +1832,15 @@ export interface operations {
requestBody?: never;
responses: {
/** @description Successful Response */
204: {
200: {
headers: {
[name: string]: unknown;
};
content?: never;
content: {
"application/json": {
[key: string]: number;
};
};
};
/** @description Validation Error */
422: {
@@ -1736,6 +1921,142 @@ export interface operations {
};
};
};
list_names_api_v1_trees__tree_id__persons__person_id__names_get: {
parameters: {
query?: never;
header?: never;
path: {
tree_id: string;
person_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["NameRead"][];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
create_name_api_v1_trees__tree_id__persons__person_id__names_post: {
parameters: {
query?: never;
header?: never;
path: {
tree_id: string;
person_id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["NameCreate"];
};
};
responses: {
/** @description Successful Response */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["NameRead"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
delete_name_api_v1_trees__tree_id__persons__person_id__names__name_id__delete: {
parameters: {
query?: never;
header?: never;
path: {
tree_id: string;
person_id: string;
name_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"];
};
};
};
};
update_name_api_v1_trees__tree_id__persons__person_id__names__name_id__patch: {
parameters: {
query?: never;
header?: never;
path: {
tree_id: string;
person_id: string;
name_id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["NameUpdate"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["NameRead"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
list_tree_events_api_v1_trees__tree_id__events_get: {
parameters: {
query?: never;
+602 -2
View File
@@ -280,6 +280,48 @@
}
}
},
"/api/v1/users/me/self-person": {
"patch": {
"tags": [
"users"
],
"summary": "Set Self Person",
"description": "Link (or unlink) the Person record that represents this account.",
"operationId": "set_self_person_api_v1_users_me_self_person_patch",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserSelfPersonUpdate"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserRead"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/v1/trees": {
"post": {
"tags": [
@@ -728,6 +770,7 @@
"persons"
],
"summary": "Delete Person",
"description": "Delete a person. ``cascade=true`` also deletes all descendants. Returns\nthe number of persons deleted (1 unless cascading).",
"operationId": "delete_person_api_v1_trees__tree_id__persons__person_id__delete",
"parameters": [
{
@@ -749,11 +792,32 @@
"format": "uuid",
"title": "Person Id"
}
},
{
"name": "cascade",
"in": "query",
"required": false,
"schema": {
"type": "boolean",
"default": false,
"title": "Cascade"
}
}
],
"responses": {
"204": {
"description": "Successful Response"
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": {
"type": "integer"
},
"title": "Response Delete Person Api V1 Trees Tree Id Persons Person Id Delete"
}
}
}
},
"422": {
"description": "Validation Error",
@@ -872,6 +936,251 @@
}
}
},
"/api/v1/trees/{tree_id}/persons/{person_id}/names": {
"get": {
"tags": [
"names"
],
"summary": "List Names",
"operationId": "list_names_api_v1_trees__tree_id__persons__person_id__names_get",
"parameters": [
{
"name": "tree_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid",
"title": "Tree Id"
}
},
{
"name": "person_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid",
"title": "Person Id"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NameRead"
},
"title": "Response List Names Api V1 Trees Tree Id Persons Person Id Names Get"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"post": {
"tags": [
"names"
],
"summary": "Create Name",
"operationId": "create_name_api_v1_trees__tree_id__persons__person_id__names_post",
"parameters": [
{
"name": "tree_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid",
"title": "Tree Id"
}
},
{
"name": "person_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid",
"title": "Person Id"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NameCreate"
}
}
}
},
"responses": {
"201": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NameRead"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/v1/trees/{tree_id}/persons/{person_id}/names/{name_id}": {
"patch": {
"tags": [
"names"
],
"summary": "Update Name",
"operationId": "update_name_api_v1_trees__tree_id__persons__person_id__names__name_id__patch",
"parameters": [
{
"name": "tree_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid",
"title": "Tree Id"
}
},
{
"name": "person_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid",
"title": "Person Id"
}
},
{
"name": "name_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid",
"title": "Name Id"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NameUpdate"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NameRead"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"delete": {
"tags": [
"names"
],
"summary": "Delete Name",
"operationId": "delete_name_api_v1_trees__tree_id__persons__person_id__names__name_id__delete",
"parameters": [
{
"name": "tree_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid",
"title": "Tree Id"
}
},
{
"name": "person_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid",
"title": "Person Id"
}
},
{
"name": "name_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid",
"title": "Name Id"
}
}
],
"responses": {
"204": {
"description": "Successful Response"
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/v1/trees/{tree_id}/events": {
"post": {
"tags": [
@@ -3140,6 +3449,267 @@
"type": "object",
"title": "MediaUpdate"
},
"NameCreate": {
"properties": {
"name_type": {
"type": "string",
"title": "Name Type",
"default": "birth"
},
"given": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Given"
},
"surname": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Surname"
},
"prefix": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Prefix"
},
"suffix": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Suffix"
},
"nickname": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Nickname"
},
"is_primary": {
"type": "boolean",
"title": "Is Primary",
"default": false
}
},
"type": "object",
"title": "NameCreate"
},
"NameRead": {
"properties": {
"id": {
"type": "string",
"format": "uuid",
"title": "Id"
},
"tree_id": {
"type": "string",
"format": "uuid",
"title": "Tree Id"
},
"person_id": {
"type": "string",
"format": "uuid",
"title": "Person Id"
},
"name_type": {
"type": "string",
"title": "Name Type"
},
"given": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Given"
},
"surname": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Surname"
},
"prefix": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Prefix"
},
"suffix": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Suffix"
},
"nickname": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Nickname"
},
"is_primary": {
"type": "boolean",
"title": "Is Primary"
},
"sort_order": {
"type": "integer",
"title": "Sort Order"
},
"created_at": {
"type": "string",
"format": "date-time",
"title": "Created At"
}
},
"type": "object",
"required": [
"id",
"tree_id",
"person_id",
"name_type",
"given",
"surname",
"prefix",
"suffix",
"nickname",
"is_primary",
"sort_order",
"created_at"
],
"title": "NameRead"
},
"NameUpdate": {
"properties": {
"name_type": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Name Type"
},
"given": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Given"
},
"surname": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Surname"
},
"prefix": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Prefix"
},
"suffix": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Suffix"
},
"nickname": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Nickname"
},
"is_primary": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"title": "Is Primary"
}
},
"type": "object",
"title": "NameUpdate"
},
"ParentChildQualifier": {
"type": "string",
"enum": [
@@ -4063,6 +4633,18 @@
],
"title": "Email Verified At"
},
"self_person_id": {
"anyOf": [
{
"type": "string",
"format": "uuid"
},
{
"type": "null"
}
],
"title": "Self Person Id"
},
"created_at": {
"type": "string",
"format": "date-time",
@@ -4079,6 +4661,24 @@
],
"title": "UserRead"
},
"UserSelfPersonUpdate": {
"properties": {
"self_person_id": {
"anyOf": [
{
"type": "string",
"format": "uuid"
},
{
"type": "null"
}
],
"title": "Self Person Id"
}
},
"type": "object",
"title": "UserSelfPersonUpdate"
},
"ValidationError": {
"properties": {
"loc": {