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
+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": {