Commit 23a07cc
authored
[Improvement: SparseTree] Implement Next Planned Feature (#70)
* feat(relationships): add manual relationship linking for parents, spouses, and children
Implements Phase 15.20 - users can now link existing people or create new
person stubs as parents, spouses, or children from the PersonDetail page.
- Add link-relationship/unlink-relationship API endpoints with validation
- Add quick-search endpoint (FTS5 with database scoping) for modal search
- Add createPersonStub to id-mapping service for manual person creation
- Create RelationshipModal with search + create tabs, debounced search
- Add "+ Add" buttons to Parents, Spouses, and Children sections
- Replace placeholder modal with functional RelationshipModal
* docs: mark Phase 15.20 (relationship linking) as complete
* fix(relationships): address PR #70 review feedback
Server fixes:
- quick-search: replace LEFT JOIN vital_event with subquery (MIN+GROUP BY)
to prevent duplicate rows when persons have multiple birth records
- link-relationship: scope source/target to dbId, reject cross-database
links, add new stubs to database_membership in same transaction
- link-relationship: validate gender against CHECK constraint values
(male/female/unknown), coerce based on relationshipType when invalid
- link-relationship: wrap stub creation + membership + edge insertion
in a single transaction so failures roll back all writes
- link-relationship: pre-check duplicates before any writes; extract
checkDuplicateEdge helper
- link/unlink-relationship: validate targetId as canonical ULID
- unlink-relationship: scope deletes via membership pre-checks; remove
redundant EXISTS guards in DELETE statements for consistency
- createPersonStub: wrap insert + FTS update in transaction
- extract isPersonInDatabase helper shared by link/unlink
Client fixes:
- RelationshipModal: handle quickSearchPersons errors with toast,
ensure searching state always clears via finally
- RelationshipModal: reset searching/linkingId on modal open so a
re-opened modal never shows stale spinner
- RelationshipModal: drop unused color field from TYPE_CONFIG
- api.ts: type linkRelationship/unlinkRelationship as RelationshipType
instead of string; move type to client/src/types/relationship.ts
- PersonDetail: extract reloadPerson helper; refresh parent/spouse/child
data after linking instead of only updating the person object
- PersonDetail: handle errors in onLinked with toast feedback
- PersonDetail: determine missing parent role from gender, not array
index (parents.filter(Boolean) collapses sparse positions)
* fix(relationships): address round 2 review feedback
- person.routes: resolve route :dbId to internal db_id via resolveDbId()
in quick-search, link-relationship, and unlink-relationship handlers,
matching the pattern used by integrity/auditor/search services. Without
this, callers passing a legacy/FamilySearch ID silently fail membership
checks and create orphan database_membership rows.
- quick-search: add ORDER BY display_name, person_id for deterministic
autocomplete results
- PersonDetail: import RelationshipType from types/relationship instead
of from RelationshipModal (reduces coupling)
- RelationshipModal: drop now-unused re-export of RelationshipType
* fix(relationships): address round 3 review feedback
- person.routes link-relationship: use INSERT OR IGNORE for edge inserts
to defend against races between pre-check and write; check changes count
and return 409 if no row was inserted (duplicate snuck in)
- person.routes link-relationship: refresh database_info.person_count
cache when a new membership row is added so the database listing
reflects new persons immediately
- api.ts: tighten linkRelationship/unlinkRelationship response types from
string to RelationshipType for full type-safety end to end
* fix(relationships): address round 4 review feedback + add tests
- person.routes link-relationship: trim newPerson.name and reject blank
or whitespace-only values to prevent stub persons with empty display names
- RelationshipModal: type onLinked as () => void | Promise<void> and
await it before closing the modal, so the modal stays open visually
until the parent's reload completes
- tests: add tests/integration/api/relationships.spec.ts with 17 cases
covering quick-search (db scoping, query length, no-match), link
(validation, self-link, cross-db, stub creation, duplicate, parent),
and unlink (delete, 404, cross-db rejection)
- tests/integration/setup: register inline simplified versions of
quick-search, link-relationship, and unlink-relationship endpoints
matching production validation, scoping, and idempotent insert behavior
* fix(relationships): address round 5 review feedback
- person.routes link-relationship: insert membership only AFTER confirming
edge insert was new. Previously, INSERT OR IGNORE racing with a
concurrent edge insert would still write the membership row even when
returning 409. Now race-induced 409s never mutate state.
- tests/setup link-relationship: same ordering fix; reword 'mirrors
production validation' comment to explicitly call out divergences
(no canonical-ULID checks, no resolveDbId, simple stub IDs)
* fix(relationships): guard doSearch against out-of-order responses
Track a monotonically increasing search request id and drop responses
whose id no longer matches the latest. Without this, fast typing could
let an earlier search resolve after a later one and overwrite results
or flip the spinner off prematurely.
* test(relationships): include birthYear in test quick-search response
Match production response shape so client/contract regressions on the
birthYear field are caught in tests.
* fix(relationships): guard against late results and mid-link unmount
- doSearch: bump request id (and clear searching) when query falls below
2 chars, so a previously-issued >=2-char request resolving late cannot
repopulate stale results
- RelationshipModal: introduce safeClose() that no-ops while a link/create
is in flight, and disable the X close button when linking. Backdrop
click and X button no longer unmount the modal mid-await, preventing
setState/onLinked from running on a dead component
* fix(relationships): drop unused async on synchronous route handlers
quick-search, link-relationship, and unlink-relationship handlers do
only synchronous SQLite work but were marked async, which would cause
thrown exceptions to become unhandled Promise rejections in Express 4
(no express-async-errors registered). Removing async lets Express
catch synchronous throws via the standard error middleware.
* fix(relationships): reject cross-database link targets
Edge tables (parent_edge, spouse_edge) are not scoped by db_id, so
silently auto-importing an existing person into the source database
during link-relationship would leak relationships across databases.
- person.routes link-relationship: require existing targets to already
be members of the source database; return 403 otherwise. Stub creation
remains the only path that adds a new membership row, since stubs are
brand-new persons that didn't exist before this request.
- tests/setup link-relationship: mirror the rejection
- tests: add coverage for the cross-database rejection (existing person
with no membership in source db → 403 + no edge inserted)
* fix(relationships): handle unknown gender + birthYear=0 in modal
- PersonDetail parent inference: filter to known male/female genders
before deciding which parent role is missing. Previously, an existing
parent with gender 'unknown' or with parentData not yet loaded would
fall through and default to 'father' even when the existing parent
was actually the father, leading to a confusing duplicate error.
- RelationshipModal: use != null instead of truthy check for birthYear,
so a valid birthYear of 0 isn't hidden and replaced with the truncated
ID fallback
* fix(relationships): normalize quick-search query param
req.query.q may be string | string[] | undefined; calling .trim()
directly on the cast crashed with a 500 if a client sent multiple
?q= parameters. Normalize to the first value before length checks.1 parent 2794da2 commit 23a07cc
9 files changed
Lines changed: 1325 additions & 101 deletions
File tree
- client/src
- components/person
- services
- types
- server/src
- routes
- services
- tests/integration
- api
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
39 | | - | |
| 39 | + | |
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
17 | 20 | | |
18 | 21 | | |
19 | 22 | | |
| |||
210 | 213 | | |
211 | 214 | | |
212 | 215 | | |
213 | | - | |
| 216 | + | |
214 | 217 | | |
215 | 218 | | |
216 | 219 | | |
| |||
221 | 224 | | |
222 | 225 | | |
223 | 226 | | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
224 | 302 | | |
225 | 303 | | |
226 | 304 | | |
| |||
260 | 338 | | |
261 | 339 | | |
262 | 340 | | |
263 | | - | |
264 | 341 | | |
265 | 342 | | |
266 | 343 | | |
267 | 344 | | |
268 | 345 | | |
269 | | - | |
| 346 | + | |
270 | 347 | | |
271 | 348 | | |
272 | | - | |
273 | 349 | | |
274 | 350 | | |
275 | 351 | | |
276 | 352 | | |
277 | | - | |
278 | | - | |
279 | | - | |
280 | | - | |
281 | | - | |
282 | | - | |
283 | | - | |
284 | | - | |
285 | | - | |
286 | | - | |
287 | | - | |
288 | | - | |
289 | | - | |
290 | | - | |
291 | | - | |
292 | | - | |
293 | | - | |
294 | | - | |
295 | | - | |
296 | | - | |
297 | | - | |
298 | | - | |
299 | | - | |
300 | | - | |
301 | | - | |
302 | | - | |
303 | | - | |
304 | | - | |
305 | | - | |
306 | | - | |
307 | | - | |
308 | | - | |
309 | | - | |
310 | | - | |
311 | | - | |
312 | | - | |
313 | | - | |
314 | | - | |
315 | | - | |
316 | | - | |
317 | | - | |
318 | | - | |
319 | | - | |
320 | | - | |
321 | | - | |
322 | | - | |
323 | | - | |
324 | | - | |
325 | | - | |
326 | | - | |
327 | | - | |
328 | | - | |
329 | | - | |
330 | | - | |
331 | | - | |
332 | | - | |
333 | | - | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
334 | 356 | | |
335 | 357 | | |
336 | 358 | | |
| |||
347 | 369 | | |
348 | 370 | | |
349 | 371 | | |
350 | | - | |
| 372 | + | |
351 | 373 | | |
352 | 374 | | |
353 | 375 | | |
| |||
960 | 982 | | |
961 | 983 | | |
962 | 984 | | |
963 | | - | |
| 985 | + | |
| 986 | + | |
964 | 987 | | |
965 | 988 | | |
| 989 | + | |
| 990 | + | |
| 991 | + | |
| 992 | + | |
| 993 | + | |
| 994 | + | |
| 995 | + | |
| 996 | + | |
| 997 | + | |
| 998 | + | |
| 999 | + | |
| 1000 | + | |
| 1001 | + | |
| 1002 | + | |
| 1003 | + | |
| 1004 | + | |
| 1005 | + | |
| 1006 | + | |
| 1007 | + | |
| 1008 | + | |
| 1009 | + | |
| 1010 | + | |
| 1011 | + | |
| 1012 | + | |
| 1013 | + | |
966 | 1014 | | |
967 | 1015 | | |
968 | 1016 | | |
| |||
993 | 1041 | | |
994 | 1042 | | |
995 | 1043 | | |
996 | | - | |
| 1044 | + | |
997 | 1045 | | |
998 | 1046 | | |
999 | 1047 | | |
| |||
1017 | 1065 | | |
1018 | 1066 | | |
1019 | 1067 | | |
1020 | | - | |
| 1068 | + | |
| 1069 | + | |
1021 | 1070 | | |
1022 | 1071 | | |
| 1072 | + | |
| 1073 | + | |
| 1074 | + | |
| 1075 | + | |
| 1076 | + | |
| 1077 | + | |
| 1078 | + | |
| 1079 | + | |
| 1080 | + | |
1023 | 1081 | | |
1024 | 1082 | | |
1025 | 1083 | | |
| |||
1281 | 1339 | | |
1282 | 1340 | | |
1283 | 1341 | | |
1284 | | - | |
1285 | | - | |
1286 | | - | |
1287 | | - | |
1288 | | - | |
1289 | | - | |
1290 | | - | |
1291 | | - | |
1292 | | - | |
1293 | | - | |
1294 | | - | |
1295 | | - | |
1296 | | - | |
1297 | | - | |
1298 | | - | |
1299 | | - | |
1300 | | - | |
1301 | | - | |
1302 | | - | |
1303 | | - | |
1304 | | - | |
1305 | | - | |
1306 | | - | |
1307 | | - | |
1308 | | - | |
1309 | | - | |
1310 | | - | |
1311 | | - | |
1312 | | - | |
1313 | | - | |
1314 | | - | |
1315 | | - | |
1316 | | - | |
1317 | | - | |
| 1342 | + | |
| 1343 | + | |
| 1344 | + | |
| 1345 | + | |
| 1346 | + | |
| 1347 | + | |
| 1348 | + | |
| 1349 | + | |
| 1350 | + | |
| 1351 | + | |
| 1352 | + | |
| 1353 | + | |
| 1354 | + | |
| 1355 | + | |
| 1356 | + | |
| 1357 | + | |
1318 | 1358 | | |
1319 | 1359 | | |
1320 | 1360 | | |
0 commit comments