@treeviz/familysearch-sdk 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1689 @@
1
+ 'use strict';
2
+
3
+ // src/client.ts
4
+ var ENVIRONMENT_CONFIGS = {
5
+ production: {
6
+ identHost: "https://ident.familysearch.org",
7
+ platformHost: "https://api.familysearch.org"
8
+ },
9
+ beta: {
10
+ identHost: "https://identbeta.familysearch.org",
11
+ platformHost: "https://apibeta.familysearch.org"
12
+ },
13
+ integration: {
14
+ identHost: "https://identint.familysearch.org",
15
+ platformHost: "https://api-integ.familysearch.org"
16
+ }
17
+ };
18
+ var noopLogger = {
19
+ log: () => {
20
+ },
21
+ warn: () => {
22
+ },
23
+ error: () => {
24
+ }
25
+ };
26
+ var FamilySearchSDK = class {
27
+ constructor(config = {}) {
28
+ this.accessToken = null;
29
+ this.appKey = null;
30
+ this.environment = config.environment || "integration";
31
+ this.accessToken = config.accessToken || null;
32
+ this.appKey = config.appKey || null;
33
+ this.logger = config.logger || noopLogger;
34
+ }
35
+ /**
36
+ * Get the current environment
37
+ */
38
+ getEnvironment() {
39
+ return this.environment;
40
+ }
41
+ /**
42
+ * Set OAuth access token
43
+ */
44
+ setAccessToken(token) {
45
+ this.accessToken = token;
46
+ }
47
+ /**
48
+ * Get current access token
49
+ */
50
+ getAccessToken() {
51
+ return this.accessToken;
52
+ }
53
+ /**
54
+ * Clear access token
55
+ */
56
+ clearAccessToken() {
57
+ this.accessToken = null;
58
+ }
59
+ /**
60
+ * Check if SDK has a valid access token
61
+ */
62
+ hasAccessToken() {
63
+ return !!this.accessToken;
64
+ }
65
+ /**
66
+ * Get environment configuration
67
+ */
68
+ getConfig() {
69
+ return ENVIRONMENT_CONFIGS[this.environment];
70
+ }
71
+ /**
72
+ * Make authenticated API request
73
+ */
74
+ async request(url, options = {}) {
75
+ const config = this.getConfig();
76
+ const fullUrl = url.startsWith("http") ? url : `${config.platformHost}${url}`;
77
+ const headers = {
78
+ Accept: "application/json",
79
+ ...options.headers
80
+ };
81
+ const requiresAuth = fullUrl.includes("/platform/");
82
+ if (this.accessToken && requiresAuth) {
83
+ headers.Authorization = `Bearer ${this.accessToken}`;
84
+ }
85
+ if (this.appKey) {
86
+ headers["X-FS-App-Key"] = this.appKey;
87
+ }
88
+ this.logger.log(
89
+ `[FamilySearch SDK] ${options.method || "GET"} ${fullUrl}`
90
+ );
91
+ try {
92
+ const response = await fetch(fullUrl, {
93
+ ...options,
94
+ headers
95
+ });
96
+ const responseHeaders = {};
97
+ response.headers.forEach((value, key) => {
98
+ responseHeaders[key] = value;
99
+ });
100
+ let data;
101
+ const contentType = response.headers.get("content-type");
102
+ if (contentType && contentType.includes("application/json")) {
103
+ try {
104
+ data = await response.json();
105
+ } catch (error) {
106
+ this.logger.warn(
107
+ "[FamilySearch SDK] Failed to parse JSON response:",
108
+ error
109
+ );
110
+ }
111
+ }
112
+ const apiResponse = {
113
+ data,
114
+ statusCode: response.status,
115
+ statusText: response.statusText,
116
+ headers: responseHeaders
117
+ };
118
+ if (!response.ok) {
119
+ const error = new Error(
120
+ `FamilySearch API error: ${response.status} ${response.statusText}`
121
+ );
122
+ error.statusCode = response.status;
123
+ error.response = apiResponse;
124
+ throw error;
125
+ }
126
+ return apiResponse;
127
+ } catch (error) {
128
+ this.logger.error("[FamilySearch SDK] Request failed:", error);
129
+ throw error;
130
+ }
131
+ }
132
+ /**
133
+ * GET request
134
+ */
135
+ async get(url, options = {}) {
136
+ return this.request(url, { ...options, method: "GET" });
137
+ }
138
+ /**
139
+ * POST request
140
+ */
141
+ async post(url, body, options = {}) {
142
+ const headers = {
143
+ "Content-Type": "application/json",
144
+ ...options.headers
145
+ };
146
+ return this.request(url, {
147
+ ...options,
148
+ method: "POST",
149
+ headers,
150
+ body: body ? JSON.stringify(body) : void 0
151
+ });
152
+ }
153
+ /**
154
+ * PUT request
155
+ */
156
+ async put(url, body, options = {}) {
157
+ const headers = {
158
+ "Content-Type": "application/json",
159
+ ...options.headers
160
+ };
161
+ return this.request(url, {
162
+ ...options,
163
+ method: "PUT",
164
+ headers,
165
+ body: body ? JSON.stringify(body) : void 0
166
+ });
167
+ }
168
+ /**
169
+ * DELETE request
170
+ */
171
+ async delete(url, options = {}) {
172
+ return this.request(url, { ...options, method: "DELETE" });
173
+ }
174
+ // ====================================
175
+ // User API
176
+ // ====================================
177
+ /**
178
+ * Get current authenticated user
179
+ */
180
+ async getCurrentUser() {
181
+ try {
182
+ const response = await this.get(
183
+ "/platform/users/current"
184
+ );
185
+ const user = response.data?.users?.[0];
186
+ return user || null;
187
+ } catch (error) {
188
+ this.logger.error(
189
+ "[FamilySearch SDK] Failed to get current user:",
190
+ error
191
+ );
192
+ return null;
193
+ }
194
+ }
195
+ // ====================================
196
+ // Tree/Pedigree API
197
+ // ====================================
198
+ /**
199
+ * Get person by ID
200
+ */
201
+ async getPerson(personId) {
202
+ try {
203
+ const response = await this.get(
204
+ `/platform/tree/persons/${personId}`
205
+ );
206
+ const person = response.data?.persons?.[0];
207
+ return person || null;
208
+ } catch (error) {
209
+ this.logger.error(
210
+ `[FamilySearch SDK] Failed to get person ${personId}:`,
211
+ error
212
+ );
213
+ return null;
214
+ }
215
+ }
216
+ /**
217
+ * Get person with full details including sources
218
+ */
219
+ async getPersonWithDetails(personId) {
220
+ try {
221
+ const response = await this.get(
222
+ `/platform/tree/persons/${personId}?sourceDescriptions=true`
223
+ );
224
+ return response.data || null;
225
+ } catch (error) {
226
+ this.logger.error(
227
+ `[FamilySearch SDK] Failed to get person details ${personId}:`,
228
+ error
229
+ );
230
+ return null;
231
+ }
232
+ }
233
+ /**
234
+ * Get notes for a person
235
+ */
236
+ async getPersonNotes(personId) {
237
+ try {
238
+ const response = await this.get(
239
+ `/platform/tree/persons/${personId}/notes`
240
+ );
241
+ return response.data || null;
242
+ } catch (error) {
243
+ this.logger.error(
244
+ `[FamilySearch SDK] Failed to get notes for ${personId}:`,
245
+ error
246
+ );
247
+ return null;
248
+ }
249
+ }
250
+ /**
251
+ * Get memories for a person
252
+ */
253
+ async getPersonMemories(personId) {
254
+ try {
255
+ const response = await this.get(
256
+ `/platform/tree/persons/${personId}/memories`
257
+ );
258
+ return response.data || null;
259
+ } catch (error) {
260
+ this.logger.error(
261
+ `[FamilySearch SDK] Failed to get memories for ${personId}:`,
262
+ error
263
+ );
264
+ return null;
265
+ }
266
+ }
267
+ /**
268
+ * Get couple relationship details
269
+ */
270
+ async getCoupleRelationship(relationshipId) {
271
+ try {
272
+ const response = await this.get(
273
+ `/platform/tree/couple-relationships/${relationshipId}`
274
+ );
275
+ return response.data || null;
276
+ } catch (error) {
277
+ this.logger.error(
278
+ `[FamilySearch SDK] Failed to get couple relationship ${relationshipId}:`,
279
+ error
280
+ );
281
+ return null;
282
+ }
283
+ }
284
+ /**
285
+ * Get child-and-parents relationship details
286
+ */
287
+ async getChildAndParentsRelationship(relationshipId) {
288
+ try {
289
+ const response = await this.get(
290
+ `/platform/tree/child-and-parents-relationships/${relationshipId}`
291
+ );
292
+ return response.data || null;
293
+ } catch (error) {
294
+ this.logger.error(
295
+ `[FamilySearch SDK] Failed to get child-and-parents relationship ${relationshipId}:`,
296
+ error
297
+ );
298
+ return null;
299
+ }
300
+ }
301
+ /**
302
+ * Get ancestry for a person
303
+ */
304
+ async getAncestry(personId, generations = 4) {
305
+ return this.get(
306
+ `/platform/tree/ancestry?person=${personId}&generations=${generations}`
307
+ );
308
+ }
309
+ /**
310
+ * Get descendancy for a person
311
+ */
312
+ async getDescendancy(personId, generations = 2) {
313
+ return this.get(
314
+ `/platform/tree/descendancy?person=${personId}&generations=${generations}`
315
+ );
316
+ }
317
+ /**
318
+ * Search for persons
319
+ */
320
+ async searchPersons(query, options = {}) {
321
+ const params = new URLSearchParams({
322
+ ...query,
323
+ ...options.start !== void 0 && {
324
+ start: options.start.toString()
325
+ },
326
+ ...options.count !== void 0 && {
327
+ count: options.count.toString()
328
+ }
329
+ });
330
+ return this.get(
331
+ `/platform/tree/search?${params.toString()}`
332
+ );
333
+ }
334
+ // ====================================
335
+ // Places API
336
+ // ====================================
337
+ /**
338
+ * Search for places
339
+ */
340
+ async searchPlaces(name, options = {}) {
341
+ try {
342
+ const params = new URLSearchParams({
343
+ name,
344
+ ...options.parentId && { parentId: options.parentId },
345
+ ...options.typeId && { typeId: options.typeId },
346
+ ...options.date && { date: options.date },
347
+ ...options.start !== void 0 && {
348
+ start: options.start.toString()
349
+ },
350
+ ...options.count !== void 0 && {
351
+ count: options.count.toString()
352
+ }
353
+ });
354
+ const response = await this.get(
355
+ `/platform/places/search?${params.toString()}`
356
+ );
357
+ return response.data || null;
358
+ } catch (error) {
359
+ this.logger.error(
360
+ "[FamilySearch SDK] Failed to search places:",
361
+ error
362
+ );
363
+ return null;
364
+ }
365
+ }
366
+ /**
367
+ * Get place by ID
368
+ */
369
+ async getPlace(placeId) {
370
+ try {
371
+ const response = await this.get(
372
+ `/platform/places/${placeId}`
373
+ );
374
+ const place = response.data?.places?.[0];
375
+ return place || null;
376
+ } catch (error) {
377
+ this.logger.error(
378
+ `[FamilySearch SDK] Failed to get place ${placeId}:`,
379
+ error
380
+ );
381
+ return null;
382
+ }
383
+ }
384
+ // ====================================
385
+ // Import/Export API
386
+ // ====================================
387
+ /**
388
+ * Get GEDCOM export for a person and their ancestors
389
+ */
390
+ async exportGEDCOM(personId) {
391
+ try {
392
+ const response = await this.get(
393
+ `/platform/tree/persons/${personId}/gedcomx`,
394
+ {
395
+ headers: {
396
+ Accept: "application/x-gedcomx-v1+json"
397
+ }
398
+ }
399
+ );
400
+ return response.data || null;
401
+ } catch (error) {
402
+ this.logger.error(
403
+ "[FamilySearch SDK] Failed to export GEDCOM:",
404
+ error
405
+ );
406
+ return null;
407
+ }
408
+ }
409
+ };
410
+ var sdkInstance = null;
411
+ function initFamilySearchSDK(config = {}) {
412
+ if (!sdkInstance) {
413
+ sdkInstance = new FamilySearchSDK(config);
414
+ } else {
415
+ if (config.accessToken !== void 0) {
416
+ sdkInstance.setAccessToken(config.accessToken);
417
+ }
418
+ if (config.environment !== void 0) {
419
+ sdkInstance = new FamilySearchSDK(config);
420
+ }
421
+ }
422
+ return sdkInstance;
423
+ }
424
+ function getFamilySearchSDK() {
425
+ if (!sdkInstance) {
426
+ sdkInstance = new FamilySearchSDK();
427
+ }
428
+ return sdkInstance;
429
+ }
430
+ function createFamilySearchSDK(config = {}) {
431
+ return new FamilySearchSDK(config);
432
+ }
433
+ function resetFamilySearchSDK() {
434
+ sdkInstance = null;
435
+ }
436
+
437
+ // src/auth/oauth.ts
438
+ var OAUTH_ENDPOINTS = {
439
+ production: {
440
+ authorization: "https://ident.familysearch.org/cis-web/oauth2/v3/authorization",
441
+ token: "https://ident.familysearch.org/cis-web/oauth2/v3/token",
442
+ currentUser: "https://api.familysearch.org/platform/users/current"
443
+ },
444
+ beta: {
445
+ authorization: "https://identbeta.familysearch.org/cis-web/oauth2/v3/authorization",
446
+ token: "https://identbeta.familysearch.org/cis-web/oauth2/v3/token",
447
+ currentUser: "https://apibeta.familysearch.org/platform/users/current"
448
+ },
449
+ integration: {
450
+ authorization: "https://identint.familysearch.org/cis-web/oauth2/v3/authorization",
451
+ token: "https://identint.familysearch.org/cis-web/oauth2/v3/token",
452
+ currentUser: "https://api-integ.familysearch.org/platform/users/current"
453
+ }
454
+ };
455
+ function getOAuthEndpoints(environment = "integration") {
456
+ return OAUTH_ENDPOINTS[environment];
457
+ }
458
+ function generateOAuthState() {
459
+ const array = new Uint8Array(32);
460
+ crypto.getRandomValues(array);
461
+ return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(
462
+ ""
463
+ );
464
+ }
465
+ function buildAuthorizationUrl(config, state, options = {}) {
466
+ const endpoints = getOAuthEndpoints(config.environment);
467
+ const url = new URL(endpoints.authorization);
468
+ url.searchParams.set("response_type", "code");
469
+ url.searchParams.set("client_id", config.clientId);
470
+ url.searchParams.set("redirect_uri", config.redirectUri);
471
+ url.searchParams.set("state", state);
472
+ if (options.scopes && options.scopes.length > 0) {
473
+ url.searchParams.set("scope", options.scopes.join(" "));
474
+ }
475
+ if (options.prompt) {
476
+ url.searchParams.set("prompt", options.prompt);
477
+ }
478
+ return url.toString();
479
+ }
480
+ async function exchangeCodeForToken(code, config) {
481
+ const endpoints = getOAuthEndpoints(config.environment);
482
+ const response = await fetch(endpoints.token, {
483
+ method: "POST",
484
+ headers: {
485
+ "Content-Type": "application/x-www-form-urlencoded",
486
+ Accept: "application/json"
487
+ },
488
+ body: new URLSearchParams({
489
+ grant_type: "authorization_code",
490
+ code,
491
+ client_id: config.clientId,
492
+ redirect_uri: config.redirectUri
493
+ })
494
+ });
495
+ if (!response.ok) {
496
+ const error = await response.text();
497
+ throw new Error(`Failed to exchange code for token: ${error}`);
498
+ }
499
+ return response.json();
500
+ }
501
+ async function refreshAccessToken(refreshToken, config) {
502
+ const endpoints = getOAuthEndpoints(config.environment);
503
+ const response = await fetch(endpoints.token, {
504
+ method: "POST",
505
+ headers: {
506
+ "Content-Type": "application/x-www-form-urlencoded",
507
+ Accept: "application/json"
508
+ },
509
+ body: new URLSearchParams({
510
+ grant_type: "refresh_token",
511
+ refresh_token: refreshToken,
512
+ client_id: config.clientId
513
+ })
514
+ });
515
+ if (!response.ok) {
516
+ const error = await response.text();
517
+ throw new Error(`Failed to refresh token: ${error}`);
518
+ }
519
+ return response.json();
520
+ }
521
+ async function validateAccessToken(accessToken, environment = "integration") {
522
+ const endpoints = getOAuthEndpoints(environment);
523
+ try {
524
+ const response = await fetch(endpoints.currentUser, {
525
+ headers: {
526
+ Authorization: `Bearer ${accessToken}`,
527
+ Accept: "application/json"
528
+ }
529
+ });
530
+ return response.ok;
531
+ } catch {
532
+ return false;
533
+ }
534
+ }
535
+ async function getUserInfo(accessToken, environment = "integration") {
536
+ const endpoints = getOAuthEndpoints(environment);
537
+ try {
538
+ const response = await fetch(endpoints.currentUser, {
539
+ headers: {
540
+ Authorization: `Bearer ${accessToken}`,
541
+ Accept: "application/json"
542
+ }
543
+ });
544
+ if (!response.ok) {
545
+ return null;
546
+ }
547
+ const data = await response.json();
548
+ const fsUser = data.users?.[0];
549
+ if (!fsUser || !fsUser.id) {
550
+ return null;
551
+ }
552
+ return {
553
+ sub: fsUser.id,
554
+ name: fsUser.contactName || fsUser.displayName,
555
+ given_name: fsUser.givenName,
556
+ family_name: fsUser.familyName,
557
+ email: fsUser.email,
558
+ email_verified: fsUser.email ? true : false
559
+ };
560
+ } catch {
561
+ return null;
562
+ }
563
+ }
564
+ var OAUTH_STORAGE_KEYS = {
565
+ state: "fs_oauth_state",
566
+ linkMode: "fs_oauth_link_mode",
567
+ lang: "fs_oauth_lang",
568
+ parentUid: "fs_oauth_parent_uid"
569
+ };
570
+ function storeOAuthState(state, options = {}) {
571
+ if (typeof localStorage === "undefined") {
572
+ throw new Error(
573
+ "localStorage is not available. For server-side usage, implement custom state storage."
574
+ );
575
+ }
576
+ localStorage.setItem(OAUTH_STORAGE_KEYS.state, state);
577
+ if (options.isLinkMode) {
578
+ localStorage.setItem(OAUTH_STORAGE_KEYS.linkMode, "true");
579
+ } else {
580
+ localStorage.removeItem(OAUTH_STORAGE_KEYS.linkMode);
581
+ }
582
+ if (options.lang) {
583
+ localStorage.setItem(OAUTH_STORAGE_KEYS.lang, options.lang);
584
+ } else {
585
+ localStorage.removeItem(OAUTH_STORAGE_KEYS.lang);
586
+ }
587
+ if (options.parentUid) {
588
+ localStorage.setItem(OAUTH_STORAGE_KEYS.parentUid, options.parentUid);
589
+ } else {
590
+ localStorage.removeItem(OAUTH_STORAGE_KEYS.parentUid);
591
+ }
592
+ }
593
+ function validateOAuthState(state) {
594
+ if (typeof localStorage === "undefined") {
595
+ return { valid: false, isLinkMode: false };
596
+ }
597
+ const storedState = localStorage.getItem(OAUTH_STORAGE_KEYS.state);
598
+ const isLinkMode = localStorage.getItem(OAUTH_STORAGE_KEYS.linkMode) === "true";
599
+ const lang = localStorage.getItem(OAUTH_STORAGE_KEYS.lang) || void 0;
600
+ const parentUid = localStorage.getItem(OAUTH_STORAGE_KEYS.parentUid) || void 0;
601
+ localStorage.removeItem(OAUTH_STORAGE_KEYS.state);
602
+ localStorage.removeItem(OAUTH_STORAGE_KEYS.linkMode);
603
+ localStorage.removeItem(OAUTH_STORAGE_KEYS.lang);
604
+ localStorage.removeItem(OAUTH_STORAGE_KEYS.parentUid);
605
+ return {
606
+ valid: storedState === state,
607
+ isLinkMode,
608
+ lang,
609
+ parentUid
610
+ };
611
+ }
612
+ function openOAuthPopup(authUrl, options = {}) {
613
+ if (typeof window === "undefined") {
614
+ throw new Error("window is not available");
615
+ }
616
+ const width = options.width || 500;
617
+ const height = options.height || 600;
618
+ const windowName = options.windowName || "FamilySearch Login";
619
+ const left = window.screenX + (window.outerWidth - width) / 2;
620
+ const top = window.screenY + (window.outerHeight - height) / 2;
621
+ const popup = window.open(
622
+ authUrl,
623
+ windowName,
624
+ `width=${width},height=${height},left=${left},top=${top},toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes`
625
+ );
626
+ if (popup) {
627
+ popup.focus();
628
+ }
629
+ return popup;
630
+ }
631
+ function parseCallbackParams(url = typeof window !== "undefined" ? window.location.href : "") {
632
+ const urlObj = new URL(url);
633
+ const params = urlObj.searchParams;
634
+ return {
635
+ code: params.get("code") || void 0,
636
+ state: params.get("state") || void 0,
637
+ error: params.get("error") || void 0,
638
+ error_description: params.get("error_description") || void 0
639
+ };
640
+ }
641
+ function getTokenStorageKey(userId, type) {
642
+ return `fs_token_${userId}_${type}`;
643
+ }
644
+ function storeTokens(userId, tokens) {
645
+ if (typeof sessionStorage === "undefined" || typeof localStorage === "undefined") {
646
+ throw new Error("Storage APIs are not available");
647
+ }
648
+ sessionStorage.setItem(
649
+ getTokenStorageKey(userId, "access"),
650
+ tokens.accessToken
651
+ );
652
+ if (tokens.expiresAt) {
653
+ sessionStorage.setItem(
654
+ getTokenStorageKey(userId, "expires"),
655
+ tokens.expiresAt.toString()
656
+ );
657
+ }
658
+ if (tokens.refreshToken) {
659
+ localStorage.setItem(
660
+ getTokenStorageKey(userId, "refresh"),
661
+ tokens.refreshToken
662
+ );
663
+ }
664
+ if (tokens.environment) {
665
+ localStorage.setItem(
666
+ getTokenStorageKey(userId, "environment"),
667
+ tokens.environment
668
+ );
669
+ }
670
+ }
671
+ function getStoredAccessToken(userId) {
672
+ if (typeof sessionStorage === "undefined") {
673
+ return null;
674
+ }
675
+ const token = sessionStorage.getItem(getTokenStorageKey(userId, "access"));
676
+ const expiresAt = sessionStorage.getItem(
677
+ getTokenStorageKey(userId, "expires")
678
+ );
679
+ if (!token) {
680
+ return null;
681
+ }
682
+ const EXPIRATION_BUFFER = 5 * 60 * 1e3;
683
+ if (expiresAt && Date.now() > parseInt(expiresAt) - EXPIRATION_BUFFER) {
684
+ return null;
685
+ }
686
+ return token;
687
+ }
688
+ function getStoredRefreshToken(userId) {
689
+ if (typeof localStorage === "undefined") {
690
+ return null;
691
+ }
692
+ return localStorage.getItem(getTokenStorageKey(userId, "refresh"));
693
+ }
694
+ function clearStoredTokens(userId) {
695
+ if (typeof sessionStorage !== "undefined") {
696
+ sessionStorage.removeItem(getTokenStorageKey(userId, "access"));
697
+ sessionStorage.removeItem(getTokenStorageKey(userId, "expires"));
698
+ }
699
+ if (typeof localStorage !== "undefined") {
700
+ localStorage.removeItem(getTokenStorageKey(userId, "refresh"));
701
+ localStorage.removeItem(getTokenStorageKey(userId, "environment"));
702
+ }
703
+ }
704
+ function clearAllTokens() {
705
+ if (typeof sessionStorage === "undefined" || typeof localStorage === "undefined") {
706
+ return;
707
+ }
708
+ const keysToRemove = [];
709
+ for (let i = 0; i < sessionStorage.length; i++) {
710
+ const key = sessionStorage.key(i);
711
+ if (key && key.startsWith("fs_token_")) {
712
+ keysToRemove.push(key);
713
+ }
714
+ }
715
+ for (let i = 0; i < localStorage.length; i++) {
716
+ const key = localStorage.key(i);
717
+ if (key && key.startsWith("fs_token_")) {
718
+ keysToRemove.push(key);
719
+ }
720
+ }
721
+ keysToRemove.forEach((key) => {
722
+ sessionStorage.removeItem(key);
723
+ localStorage.removeItem(key);
724
+ });
725
+ }
726
+
727
+ // src/places/places.ts
728
+ async function searchPlaces(sdk, query, options) {
729
+ const queryParts = [`name:"${query}"`];
730
+ if (options?.date) {
731
+ queryParts.push(`+date:${options.date}`);
732
+ }
733
+ if (options?.parentId) {
734
+ queryParts.push(`+parentId:${options.parentId}`);
735
+ }
736
+ const finalQuery = queryParts.join(" ");
737
+ const params = new URLSearchParams({
738
+ q: finalQuery,
739
+ ...options?.count && { count: options.count.toString() },
740
+ ...options?.start && { start: options.start.toString() }
741
+ });
742
+ const response = await sdk.get(
743
+ `/platform/places/search?${params.toString()}`
744
+ );
745
+ const data = response.data || {};
746
+ if (!data?.entries?.length) {
747
+ if (options?.date) {
748
+ const { date: _date, ...optionsWithoutDate } = options;
749
+ return searchPlaces(sdk, query, optionsWithoutDate);
750
+ }
751
+ return [];
752
+ }
753
+ return data.entries?.map((entry) => {
754
+ const place = entry.content?.gedcomx?.places?.[0];
755
+ return {
756
+ id: place?.id || entry.id,
757
+ title: entry.title,
758
+ fullyQualifiedName: entry.title,
759
+ names: place?.names,
760
+ standardized: place?.jurisdiction ? {
761
+ id: place.jurisdiction.id,
762
+ fullyQualifiedName: place.jurisdiction.name
763
+ } : void 0,
764
+ jurisdiction: place?.jurisdiction,
765
+ temporalDescription: place?.temporalDescription
766
+ };
767
+ }) || [];
768
+ }
769
+ async function getPlaceById(sdk, id) {
770
+ const response = await sdk.get(
771
+ `/platform/places/${id}`
772
+ );
773
+ return response.data?.places?.[0] || null;
774
+ }
775
+ async function getPlaceChildren(sdk, id, options) {
776
+ const params = new URLSearchParams({
777
+ ...options?.count && { count: options.count.toString() },
778
+ ...options?.start && { start: options.start.toString() }
779
+ });
780
+ const queryString = params.toString();
781
+ const url = `/platform/places/${id}/children${queryString ? `?${queryString}` : ""}`;
782
+ const response = await sdk.get(url);
783
+ const data = response.data || {};
784
+ return data.entries?.map((entry) => {
785
+ const place = entry.content?.gedcomx?.places?.[0];
786
+ return {
787
+ id: place?.id || entry.id,
788
+ title: entry.title,
789
+ fullyQualifiedName: entry.title,
790
+ names: place?.names,
791
+ jurisdiction: place?.jurisdiction
792
+ };
793
+ }) || [];
794
+ }
795
+ async function getPlaceDetails(sdk, id) {
796
+ const place = await getPlaceById(sdk, id);
797
+ if (!place) {
798
+ return null;
799
+ }
800
+ const primaryName = place.names?.find((n) => n.lang === "en")?.value || place.names?.[0]?.value || "";
801
+ const aliases = place.names?.filter((n) => n.value !== primaryName).map((n) => n.value || "").filter(Boolean) || [];
802
+ return {
803
+ id: place.id || id,
804
+ name: primaryName,
805
+ standardizedName: place.jurisdiction?.name,
806
+ aliases,
807
+ latitude: place.latitude,
808
+ longitude: place.longitude,
809
+ jurisdiction: place.jurisdiction?.name
810
+ };
811
+ }
812
+
813
+ // src/tree/pedigree.ts
814
+ async function fetchPedigree(sdk, personId, options = {}) {
815
+ const {
816
+ generations = 4,
817
+ onProgress,
818
+ includeDetails = true,
819
+ includeNotes = true,
820
+ includeRelationshipDetails = true
821
+ } = options;
822
+ let targetPersonId = personId;
823
+ if (!targetPersonId) {
824
+ onProgress?.({
825
+ stage: "getting_current_user",
826
+ current: 0,
827
+ total: 1,
828
+ percent: 0
829
+ });
830
+ const currentUser = await sdk.getCurrentUser();
831
+ targetPersonId = currentUser?.personId || currentUser?.treeUserId || currentUser?.id;
832
+ if (!targetPersonId) {
833
+ throw new Error("Could not determine person ID for current user");
834
+ }
835
+ }
836
+ onProgress?.({
837
+ stage: "fetching_ancestry_structure",
838
+ current: 1,
839
+ total: 3,
840
+ percent: 10
841
+ });
842
+ const ancestryResponse = await sdk.getAncestry(targetPersonId, generations);
843
+ const ancestry = ancestryResponse.data;
844
+ if (!ancestry.persons || ancestry.persons.length === 0) {
845
+ throw new Error("No persons found in ancestry");
846
+ }
847
+ onProgress?.({
848
+ stage: "fetching_person_details",
849
+ current: 0,
850
+ total: ancestry.persons.length,
851
+ percent: 20
852
+ });
853
+ const personsWithDetails = [];
854
+ for (let i = 0; i < ancestry.persons.length; i++) {
855
+ const person = ancestry.persons[i];
856
+ onProgress?.({
857
+ stage: "fetching_person_details",
858
+ current: i + 1,
859
+ total: ancestry.persons.length,
860
+ percent: 20 + Math.floor((i + 1) / ancestry.persons.length * 45)
861
+ });
862
+ try {
863
+ const enhanced = { ...person };
864
+ if (includeDetails) {
865
+ enhanced.fullDetails = await sdk.getPersonWithDetails(
866
+ person.id
867
+ );
868
+ }
869
+ if (includeNotes) {
870
+ enhanced.notes = await sdk.getPersonNotes(
871
+ person.id
872
+ );
873
+ }
874
+ personsWithDetails.push(enhanced);
875
+ } catch {
876
+ personsWithDetails.push(person);
877
+ }
878
+ }
879
+ onProgress?.({
880
+ stage: "fetching_relationship_details",
881
+ current: 0,
882
+ total: ancestry.relationships?.length || 0,
883
+ percent: 65
884
+ });
885
+ const relationshipsWithDetails = [];
886
+ if (ancestry.relationships && includeRelationshipDetails) {
887
+ for (let i = 0; i < ancestry.relationships.length; i++) {
888
+ const rel = ancestry.relationships[i];
889
+ onProgress?.({
890
+ stage: "fetching_relationship_details",
891
+ current: i + 1,
892
+ total: ancestry.relationships.length,
893
+ percent: 65 + Math.floor((i + 1) / ancestry.relationships.length * 25)
894
+ });
895
+ try {
896
+ if (rel.type?.includes?.("Couple")) {
897
+ const relDetails = await sdk.getCoupleRelationship(rel.id);
898
+ relationshipsWithDetails.push({
899
+ ...rel,
900
+ details: relDetails
901
+ });
902
+ } else {
903
+ relationshipsWithDetails.push(rel);
904
+ }
905
+ } catch {
906
+ relationshipsWithDetails.push(rel);
907
+ }
908
+ }
909
+ } else if (ancestry.relationships) {
910
+ relationshipsWithDetails.push(...ancestry.relationships);
911
+ }
912
+ onProgress?.({
913
+ stage: "extracting_additional_relationships",
914
+ current: 0,
915
+ total: 1,
916
+ percent: 90
917
+ });
918
+ const allRelationships = [...relationshipsWithDetails];
919
+ const relationshipIds = new Set(relationshipsWithDetails.map((r) => r.id));
920
+ personsWithDetails.forEach((person) => {
921
+ const childAndParentsRels = person.fullDetails?.childAndParentsRelationships;
922
+ if (childAndParentsRels && Array.isArray(childAndParentsRels)) {
923
+ childAndParentsRels.forEach((rel) => {
924
+ if (!relationshipIds.has(rel.id)) {
925
+ relationshipIds.add(rel.id);
926
+ allRelationships.push({
927
+ id: rel.id,
928
+ type: "http://gedcomx.org/ParentChild",
929
+ person1: rel.parent1,
930
+ person2: rel.child,
931
+ parent2: rel.parent2
932
+ });
933
+ }
934
+ });
935
+ }
936
+ const relationships = person.fullDetails?.relationships;
937
+ if (relationships && Array.isArray(relationships)) {
938
+ relationships.forEach((rel) => {
939
+ if (rel.type?.includes?.("Couple") && !relationshipIds.has(rel.id)) {
940
+ relationshipIds.add(rel.id);
941
+ allRelationships.push({
942
+ id: rel.id,
943
+ type: rel.type,
944
+ person1: rel.person1,
945
+ person2: rel.person2,
946
+ facts: rel.facts
947
+ });
948
+ }
949
+ });
950
+ }
951
+ });
952
+ onProgress?.({
953
+ stage: "completing_data_fetch",
954
+ current: 1,
955
+ total: 1,
956
+ percent: 98
957
+ });
958
+ return {
959
+ persons: personsWithDetails,
960
+ relationships: allRelationships,
961
+ environment: sdk.getEnvironment()
962
+ };
963
+ }
964
+ async function getCurrentUser(sdk) {
965
+ return sdk.getCurrentUser();
966
+ }
967
+ async function getPersonWithDetails(sdk, personId) {
968
+ try {
969
+ const [details, notes] = await Promise.all([
970
+ sdk.getPersonWithDetails(personId),
971
+ sdk.getPersonNotes(personId)
972
+ ]);
973
+ if (!details) {
974
+ return null;
975
+ }
976
+ const fullDetails = details;
977
+ const personData = fullDetails?.persons?.[0];
978
+ if (!personData) {
979
+ return null;
980
+ }
981
+ return {
982
+ ...personData,
983
+ fullDetails,
984
+ notes
985
+ };
986
+ } catch {
987
+ return null;
988
+ }
989
+ }
990
+ async function fetchMultiplePersons(sdk, personIds) {
991
+ const pids = personIds.join(",");
992
+ const response = await sdk.get(`/platform/tree/persons?pids=${pids}`);
993
+ return response.data;
994
+ }
995
+
996
+ // src/utils/gedcom-converter.ts
997
+ function transformFamilySearchUrl(url) {
998
+ if (!url.includes("/platform/tree/persons/")) {
999
+ return url;
1000
+ }
1001
+ const personId = url.match(/persons\/([^/?]+)/)?.[1];
1002
+ if (!personId) {
1003
+ return url;
1004
+ }
1005
+ let baseUrl = "https://www.familysearch.org";
1006
+ if (url.includes("integration.familysearch.org") || url.includes("api-integ.familysearch.org")) {
1007
+ baseUrl = "https://integration.familysearch.org";
1008
+ } else if (url.includes("beta.familysearch.org") || url.includes("apibeta.familysearch.org")) {
1009
+ baseUrl = "https://beta.familysearch.org";
1010
+ }
1011
+ return `${baseUrl}/tree/person/${personId}`;
1012
+ }
1013
+ function transformSourceUrl(url) {
1014
+ if (!url) return url;
1015
+ if (!url.includes("/platform/") && url.includes("ark:")) {
1016
+ return url;
1017
+ }
1018
+ const arkMatch = url.match(/ark:\/[^/?]+\/[^/?]+/);
1019
+ if (!arkMatch) {
1020
+ return url;
1021
+ }
1022
+ const arkId = arkMatch[0];
1023
+ let baseUrl = "https://www.familysearch.org";
1024
+ if (url.includes("integration.familysearch.org") || url.includes("api-integ.familysearch.org")) {
1025
+ baseUrl = "https://integration.familysearch.org";
1026
+ } else if (url.includes("beta.familysearch.org") || url.includes("apibeta.familysearch.org")) {
1027
+ baseUrl = "https://beta.familysearch.org";
1028
+ }
1029
+ return `${baseUrl}/${arkId}`;
1030
+ }
1031
+ function extractName(person) {
1032
+ if (!person) return null;
1033
+ if (person.names?.[0]?.nameForms?.[0]) {
1034
+ const nameForm = person.names[0].nameForms[0];
1035
+ const parts = nameForm.parts || [];
1036
+ const given = parts.find((p) => p.type?.includes("Given"))?.value || "";
1037
+ const surname = parts.find((p) => p.type?.includes("Surname"))?.value || "";
1038
+ if (given || surname) {
1039
+ return `${given} /${surname}/`.trim();
1040
+ }
1041
+ if (nameForm.fullText) {
1042
+ return nameForm.fullText;
1043
+ }
1044
+ }
1045
+ if (person.display?.name) {
1046
+ return person.display.name;
1047
+ }
1048
+ return null;
1049
+ }
1050
+ function extractGender(person) {
1051
+ if (!person) return null;
1052
+ if (person.gender?.type?.includes("Male")) {
1053
+ return "M";
1054
+ } else if (person.gender?.type?.includes("Female")) {
1055
+ return "F";
1056
+ } else if (person.display?.gender === "Male") {
1057
+ return "M";
1058
+ } else if (person.display?.gender === "Female") {
1059
+ return "F";
1060
+ }
1061
+ return null;
1062
+ }
1063
+ function extractFact(person, factType) {
1064
+ if (!person) return null;
1065
+ const result = {};
1066
+ const factTypeUrl = `http://gedcomx.org/${factType === "BIRTH" ? "Birth" : "Death"}`;
1067
+ const fact = person.facts?.find((f) => f.type === factTypeUrl);
1068
+ if (fact) {
1069
+ if (fact.date?.formal || fact.date?.original) {
1070
+ result.date = (fact.date?.formal || fact.date?.original)?.replace(
1071
+ /^(-|\+)/,
1072
+ ""
1073
+ );
1074
+ }
1075
+ if (fact.place?.original) {
1076
+ result.place = fact.place.original;
1077
+ }
1078
+ }
1079
+ if (factType === "BIRTH") {
1080
+ result.date = result.date || person.display?.birthDate;
1081
+ result.place = result.place || person.display?.birthPlace;
1082
+ } else if (factType === "DEATH") {
1083
+ result.date = result.date || person.display?.deathDate;
1084
+ result.place = result.place || person.display?.deathPlace;
1085
+ }
1086
+ return result.date || result.place ? result : null;
1087
+ }
1088
+ function formatDateForGedcom(date) {
1089
+ const months = [
1090
+ "JAN",
1091
+ "FEB",
1092
+ "MAR",
1093
+ "APR",
1094
+ "MAY",
1095
+ "JUN",
1096
+ "JUL",
1097
+ "AUG",
1098
+ "SEP",
1099
+ "OCT",
1100
+ "NOV",
1101
+ "DEC"
1102
+ ];
1103
+ const day = date.getDate();
1104
+ const month = months[date.getMonth()];
1105
+ const year = date.getFullYear();
1106
+ return `${day} ${month} ${year}`;
1107
+ }
1108
+ function convertFactToGedcom(fact) {
1109
+ const lines = [];
1110
+ if (!fact.type) return lines;
1111
+ const typeMap = {
1112
+ "http://gedcomx.org/Burial": "BURI",
1113
+ "http://gedcomx.org/Christening": "CHR",
1114
+ "http://gedcomx.org/Baptism": "BAPM",
1115
+ "http://gedcomx.org/Marriage": "MARR",
1116
+ "http://gedcomx.org/Divorce": "DIV",
1117
+ "http://gedcomx.org/Residence": "RESI",
1118
+ "http://gedcomx.org/Occupation": "OCCU",
1119
+ "http://gedcomx.org/Immigration": "IMMI",
1120
+ "http://gedcomx.org/Emigration": "EMIG",
1121
+ "http://gedcomx.org/Naturalization": "NATU",
1122
+ "http://gedcomx.org/Census": "CENS"
1123
+ };
1124
+ const gedcomTag = typeMap[fact.type] || "EVEN";
1125
+ if (gedcomTag === "OCCU" && fact.value) {
1126
+ lines.push(`1 OCCU ${fact.value}`);
1127
+ } else {
1128
+ lines.push(`1 ${gedcomTag}`);
1129
+ }
1130
+ if (fact.links?.conclusion?.href) {
1131
+ const webUrl = transformFamilySearchUrl(fact.links.conclusion.href);
1132
+ lines.push(`2 _FS_LINK ${webUrl}`);
1133
+ }
1134
+ if (fact.date?.formal || fact.date?.original) {
1135
+ lines.push(
1136
+ `2 DATE ${(fact.date?.formal || fact.date?.original)?.replace(/^(-|\+)/, "")}`
1137
+ );
1138
+ }
1139
+ if (fact.place?.original) {
1140
+ lines.push(`2 PLAC ${fact.place.original}`);
1141
+ }
1142
+ if (fact.value && gedcomTag !== "OCCU") {
1143
+ lines.push(`2 NOTE ${fact.value}`);
1144
+ }
1145
+ return lines;
1146
+ }
1147
+ function convertToGedcom(pedigreeData, options = {}) {
1148
+ const {
1149
+ treeName = "FamilySearch Import",
1150
+ includeLinks = true,
1151
+ includeNotes = true,
1152
+ environment = "production"
1153
+ } = options;
1154
+ if (!pedigreeData || !pedigreeData.persons) {
1155
+ throw new Error("Invalid FamilySearch data: no persons found");
1156
+ }
1157
+ const lines = [];
1158
+ lines.push("0 HEAD");
1159
+ lines.push("1 SOUR FamilySearch");
1160
+ lines.push("2 VERS 1.0");
1161
+ lines.push("2 NAME FamilySearch API");
1162
+ lines.push("1 DEST ANY");
1163
+ lines.push("1 DATE " + formatDateForGedcom(/* @__PURE__ */ new Date()));
1164
+ lines.push("1 SUBM @SUBM1@");
1165
+ lines.push("1 FILE " + treeName);
1166
+ lines.push("1 GEDC");
1167
+ lines.push("2 VERS 5.5");
1168
+ lines.push("2 FORM LINEAGE-LINKED");
1169
+ lines.push("1 CHAR UTF-8");
1170
+ lines.push("0 @SUBM1@ SUBM");
1171
+ const sourceMap = /* @__PURE__ */ new Map();
1172
+ let sourceCounter = 1;
1173
+ pedigreeData.persons.forEach((person) => {
1174
+ const sourceDescriptions = person.fullDetails?.sourceDescriptions;
1175
+ if (sourceDescriptions && Array.isArray(sourceDescriptions)) {
1176
+ sourceDescriptions.forEach((source) => {
1177
+ if (source.id && !sourceMap.has(source.id)) {
1178
+ sourceMap.set(source.id, source);
1179
+ }
1180
+ });
1181
+ }
1182
+ });
1183
+ sourceMap.forEach((source) => {
1184
+ const sourceId = `@S${sourceCounter++}@`;
1185
+ lines.push(`0 ${sourceId} SOUR`);
1186
+ const title = source.titles?.[0]?.value || "FamilySearch Source";
1187
+ lines.push(`1 TITL ${title}`);
1188
+ if (source.citations?.[0]?.value) {
1189
+ lines.push(`1 TEXT ${source.citations[0].value}`);
1190
+ }
1191
+ if (source.about) {
1192
+ const webUrl = transformSourceUrl(source.about);
1193
+ lines.push(`1 WWW ${webUrl}`);
1194
+ }
1195
+ if (source.resourceType) {
1196
+ lines.push(`1 NOTE Resource Type: ${source.resourceType}`);
1197
+ }
1198
+ });
1199
+ const personIdMap = /* @__PURE__ */ new Map();
1200
+ pedigreeData.persons.forEach((person, index) => {
1201
+ const gedcomId = `@I${index + 1}@`;
1202
+ personIdMap.set(person.id, gedcomId);
1203
+ });
1204
+ const families = /* @__PURE__ */ new Map();
1205
+ const childToParents = /* @__PURE__ */ new Map();
1206
+ const allRelationships = [
1207
+ ...pedigreeData.relationships || []
1208
+ ];
1209
+ pedigreeData.persons.forEach((person) => {
1210
+ const personRelationships = person.fullDetails?.relationships;
1211
+ if (personRelationships && Array.isArray(personRelationships)) {
1212
+ personRelationships.forEach((rel) => {
1213
+ const exists = allRelationships.some((r) => r.id === rel.id);
1214
+ if (!exists) {
1215
+ allRelationships.push(rel);
1216
+ }
1217
+ });
1218
+ }
1219
+ const childAndParentsRels = person.fullDetails?.childAndParentsRelationships;
1220
+ if (childAndParentsRels && Array.isArray(childAndParentsRels)) {
1221
+ childAndParentsRels.forEach((capRel) => {
1222
+ if (capRel.parent1 && capRel.child) {
1223
+ const rel = {
1224
+ id: `${capRel.id}-p1`,
1225
+ type: "http://gedcomx.org/ParentChild",
1226
+ person1: { resourceId: capRel.parent1.resourceId },
1227
+ person2: { resourceId: capRel.child.resourceId },
1228
+ parent2: capRel.parent2 ? { resourceId: capRel.parent2.resourceId } : void 0
1229
+ };
1230
+ const exists = allRelationships.some(
1231
+ (r) => r.id === rel.id
1232
+ );
1233
+ if (!exists) {
1234
+ allRelationships.push(rel);
1235
+ }
1236
+ }
1237
+ if (capRel.parent2 && capRel.child) {
1238
+ const rel = {
1239
+ id: `${capRel.id}-p2`,
1240
+ type: "http://gedcomx.org/ParentChild",
1241
+ person1: { resourceId: capRel.parent2.resourceId },
1242
+ person2: { resourceId: capRel.child.resourceId },
1243
+ parent2: capRel.parent1 ? { resourceId: capRel.parent1.resourceId } : void 0
1244
+ };
1245
+ const exists = allRelationships.some(
1246
+ (r) => r.id === rel.id
1247
+ );
1248
+ if (!exists) {
1249
+ allRelationships.push(rel);
1250
+ }
1251
+ }
1252
+ });
1253
+ }
1254
+ });
1255
+ const validPersonIds = new Set(pedigreeData.persons.map((p) => p.id));
1256
+ allRelationships.forEach((rel) => {
1257
+ if (rel.type?.includes("ParentChild")) {
1258
+ const parentId = rel.person1?.resourceId;
1259
+ const childId = rel.person2?.resourceId;
1260
+ const parent2Id = rel.parent2?.resourceId;
1261
+ if (!parentId || !childId) {
1262
+ return;
1263
+ }
1264
+ if (!validPersonIds.has(childId)) {
1265
+ return;
1266
+ }
1267
+ const validParents = [];
1268
+ if (validPersonIds.has(parentId)) {
1269
+ validParents.push(parentId);
1270
+ }
1271
+ if (parent2Id && validPersonIds.has(parent2Id)) {
1272
+ validParents.push(parent2Id);
1273
+ }
1274
+ if (validParents.length > 0) {
1275
+ if (!childToParents.has(childId)) {
1276
+ childToParents.set(childId, []);
1277
+ }
1278
+ const parents = childToParents.get(childId);
1279
+ validParents.forEach((parentId2) => {
1280
+ if (!parents.includes(parentId2)) {
1281
+ parents.push(parentId2);
1282
+ }
1283
+ });
1284
+ }
1285
+ } else if (rel.type?.includes("Couple")) {
1286
+ const person1 = rel.person1?.resourceId;
1287
+ const person2 = rel.person2?.resourceId;
1288
+ if (!person1 || !person2) {
1289
+ return;
1290
+ }
1291
+ if (validPersonIds.has(person1) && validPersonIds.has(person2)) {
1292
+ const famKey = [person1, person2].sort().join("-");
1293
+ if (!families.has(famKey)) {
1294
+ families.set(famKey, {
1295
+ spouses: /* @__PURE__ */ new Set([person1, person2]),
1296
+ children: []
1297
+ });
1298
+ }
1299
+ }
1300
+ }
1301
+ });
1302
+ childToParents.forEach((parents, childId) => {
1303
+ if (parents.length >= 2) {
1304
+ const famKey = parents.slice(0, 2).sort().join("-");
1305
+ if (!families.has(famKey)) {
1306
+ families.set(famKey, {
1307
+ spouses: new Set(parents.slice(0, 2)),
1308
+ children: []
1309
+ });
1310
+ }
1311
+ families.get(famKey).children.push(childId);
1312
+ } else if (parents.length === 1) {
1313
+ const famKey = `single-${parents[0]}`;
1314
+ if (!families.has(famKey)) {
1315
+ families.set(famKey, {
1316
+ spouses: /* @__PURE__ */ new Set([parents[0]]),
1317
+ children: []
1318
+ });
1319
+ }
1320
+ families.get(famKey).children.push(childId);
1321
+ }
1322
+ });
1323
+ const connectablePersons = /* @__PURE__ */ new Set();
1324
+ if (options.ancestryPersonIds && options.ancestryPersonIds.size > 0) {
1325
+ options.ancestryPersonIds.forEach((personId) => {
1326
+ connectablePersons.add(personId);
1327
+ });
1328
+ families.forEach((family) => {
1329
+ const spouses = Array.from(family.spouses);
1330
+ if (spouses.some((spouseId) => connectablePersons.has(spouseId))) {
1331
+ spouses.forEach((spouseId) => connectablePersons.add(spouseId));
1332
+ }
1333
+ });
1334
+ let addedNewPersons = true;
1335
+ while (addedNewPersons) {
1336
+ addedNewPersons = false;
1337
+ const beforeSize = connectablePersons.size;
1338
+ childToParents.forEach((parents, childId) => {
1339
+ if (parents.some((parentId) => connectablePersons.has(parentId))) {
1340
+ if (!connectablePersons.has(childId)) {
1341
+ connectablePersons.add(childId);
1342
+ addedNewPersons = true;
1343
+ }
1344
+ }
1345
+ });
1346
+ families.forEach((family) => {
1347
+ const spouses = Array.from(family.spouses);
1348
+ const hasConnectableSpouse = spouses.some(
1349
+ (spouseId) => connectablePersons.has(spouseId)
1350
+ );
1351
+ const hasConnectableChild = family.children.some(
1352
+ (childId) => connectablePersons.has(childId)
1353
+ );
1354
+ if (hasConnectableSpouse || hasConnectableChild) {
1355
+ spouses.forEach((spouseId) => {
1356
+ if (!connectablePersons.has(spouseId)) {
1357
+ connectablePersons.add(spouseId);
1358
+ addedNewPersons = true;
1359
+ }
1360
+ });
1361
+ }
1362
+ });
1363
+ const afterSize = connectablePersons.size;
1364
+ if (afterSize > beforeSize) {
1365
+ addedNewPersons = afterSize !== beforeSize;
1366
+ }
1367
+ }
1368
+ } else {
1369
+ childToParents.forEach((parents, childId) => {
1370
+ connectablePersons.add(childId);
1371
+ parents.forEach((parentId) => connectablePersons.add(parentId));
1372
+ });
1373
+ }
1374
+ const allowOrphanFamilies = options.allowOrphanFamilies === true;
1375
+ const orphanFamKeys = /* @__PURE__ */ new Set();
1376
+ const personToFamKeys = /* @__PURE__ */ new Map();
1377
+ families.forEach((family, key) => {
1378
+ const spouseArray = Array.from(family.spouses);
1379
+ const anyMemberConnectable = spouseArray.some((spouseId) => connectablePersons.has(spouseId)) || family.children.some((childId) => connectablePersons.has(childId));
1380
+ const isOrphanFamily = !anyMemberConnectable;
1381
+ if (isOrphanFamily) {
1382
+ orphanFamKeys.add(key);
1383
+ }
1384
+ spouseArray.forEach((spouseId) => {
1385
+ if (!personToFamKeys.has(spouseId))
1386
+ personToFamKeys.set(spouseId, /* @__PURE__ */ new Set());
1387
+ personToFamKeys.get(spouseId).add(key);
1388
+ });
1389
+ family.children.forEach((childId) => {
1390
+ if (!personToFamKeys.has(childId))
1391
+ personToFamKeys.set(childId, /* @__PURE__ */ new Set());
1392
+ personToFamKeys.get(childId).add(key);
1393
+ });
1394
+ });
1395
+ pedigreeData.persons.forEach((person) => {
1396
+ const gedcomId = personIdMap.get(person.id);
1397
+ if (!gedcomId) {
1398
+ console.warn(
1399
+ `[GEDCOM] Person ${person.id} not found in personIdMap`
1400
+ );
1401
+ return;
1402
+ }
1403
+ if (!allowOrphanFamilies) {
1404
+ const famKeys = personToFamKeys.get(person.id);
1405
+ if (famKeys && famKeys.size > 0 && Array.from(famKeys).every((key) => orphanFamKeys.has(key))) {
1406
+ return;
1407
+ }
1408
+ }
1409
+ lines.push(`0 ${gedcomId} INDI`);
1410
+ if (person.id) {
1411
+ lines.push(`1 _FS_ID ${person.id}`);
1412
+ }
1413
+ if (includeLinks) {
1414
+ const personLink = person.links?.person?.href || person.identifiers?.["http://gedcomx.org/Persistent"]?.[0];
1415
+ if (personLink) {
1416
+ const webUrl = transformFamilySearchUrl(personLink);
1417
+ lines.push(`1 _FS_LINK ${webUrl}`);
1418
+ } else if (person.id) {
1419
+ let baseUrl = "https://www.familysearch.org";
1420
+ if (environment === "beta") {
1421
+ baseUrl = "https://beta.familysearch.org";
1422
+ } else if (environment === "integration") {
1423
+ baseUrl = "https://integration.familysearch.org";
1424
+ }
1425
+ const webUrl = `${baseUrl}/tree/person/${person.id}`;
1426
+ lines.push(`1 _FS_LINK ${webUrl}`);
1427
+ }
1428
+ }
1429
+ const personData = person.fullDetails?.persons?.[0] || person;
1430
+ const name = extractName(personData);
1431
+ if (name) {
1432
+ lines.push(`1 NAME ${name}`);
1433
+ }
1434
+ const gender = extractGender(personData);
1435
+ if (gender) {
1436
+ lines.push(`1 SEX ${gender}`);
1437
+ }
1438
+ const birth = extractFact(personData, "BIRTH");
1439
+ if (birth) {
1440
+ lines.push("1 BIRT");
1441
+ if (birth.date) {
1442
+ lines.push(`2 DATE ${birth.date}`);
1443
+ }
1444
+ if (birth.place) {
1445
+ lines.push(`2 PLAC ${birth.place}`);
1446
+ }
1447
+ }
1448
+ const death = extractFact(personData, "DEATH");
1449
+ if (death) {
1450
+ lines.push("1 DEAT");
1451
+ if (death.date) {
1452
+ lines.push(`2 DATE ${death.date}`);
1453
+ }
1454
+ if (death.place) {
1455
+ lines.push(`2 PLAC ${death.place}`);
1456
+ }
1457
+ }
1458
+ personData.facts?.forEach((fact) => {
1459
+ if (fact.type && fact.type !== "http://gedcomx.org/Birth" && fact.type !== "http://gedcomx.org/Death") {
1460
+ const factLines = convertFactToGedcom(fact);
1461
+ factLines.forEach((line) => lines.push(line));
1462
+ }
1463
+ });
1464
+ if (includeNotes && person.notes?.persons?.[0]?.notes) {
1465
+ person.notes.persons[0].notes.forEach((note) => {
1466
+ if (note.text) {
1467
+ const noteText = note.text.replace(/\n/g, " ");
1468
+ lines.push(`1 NOTE ${noteText}`);
1469
+ }
1470
+ });
1471
+ }
1472
+ const personSources = personData?.sources;
1473
+ if (personSources && Array.isArray(personSources)) {
1474
+ personSources.forEach((sourceRef) => {
1475
+ const sourceId = sourceRef.descriptionId;
1476
+ if (!sourceId) return;
1477
+ const sourceIds = Array.from(sourceMap.keys());
1478
+ const sourceIndex = sourceIds.indexOf(sourceId);
1479
+ if (sourceIndex === -1) return;
1480
+ const gedcomSourceId = `@S${sourceIndex + 1}@`;
1481
+ lines.push(`1 SOUR ${gedcomSourceId}`);
1482
+ if (sourceRef.qualifiers && Array.isArray(sourceRef.qualifiers)) {
1483
+ sourceRef.qualifiers.forEach((qualifier) => {
1484
+ if (qualifier.name && qualifier.value) {
1485
+ lines.push(
1486
+ `2 NOTE ${qualifier.name}: ${qualifier.value}`
1487
+ );
1488
+ }
1489
+ });
1490
+ }
1491
+ });
1492
+ }
1493
+ });
1494
+ let famIndex = 1;
1495
+ const familyIdMap = /* @__PURE__ */ new Map();
1496
+ families.forEach((family, key) => {
1497
+ const spouseArray = Array.from(family.spouses);
1498
+ const parentsInMap = spouseArray.filter((id) => personIdMap.has(id));
1499
+ const hasNoParents = parentsInMap.length === 0;
1500
+ const hasSingleParentNoChildren = parentsInMap.length === 1 && family.children.length === 0;
1501
+ if (hasNoParents) {
1502
+ if (family.children.length > 0) {
1503
+ console.warn(
1504
+ `[FS SDK GEDCOM] Skipping FAM for orphaned child(ren): ${family.children.length} child(ren) with 0 parents`
1505
+ );
1506
+ }
1507
+ return;
1508
+ }
1509
+ if (hasSingleParentNoChildren) {
1510
+ return;
1511
+ }
1512
+ const anyMemberConnectable = spouseArray.some((spouseId) => connectablePersons.has(spouseId)) || family.children.some((childId) => connectablePersons.has(childId));
1513
+ const isOrphanFamily = !anyMemberConnectable;
1514
+ if (isOrphanFamily && !allowOrphanFamilies) {
1515
+ return;
1516
+ }
1517
+ const famId = `@F${famIndex++}@`;
1518
+ familyIdMap.set(key, famId);
1519
+ lines.push(`0 ${famId} FAM`);
1520
+ if (isOrphanFamily && allowOrphanFamilies) {
1521
+ lines.push(`1 _IS_ORPHAN_FAMILY Y`);
1522
+ }
1523
+ if (spouseArray.length === 2) {
1524
+ const [person1, person2] = spouseArray;
1525
+ const person1Data = pedigreeData.persons?.find(
1526
+ (p) => p.id === person1
1527
+ );
1528
+ const person2Data = pedigreeData.persons?.find(
1529
+ (p) => p.id === person2
1530
+ );
1531
+ const gender1 = extractGender(
1532
+ person1Data?.fullDetails?.persons?.[0] || person1Data
1533
+ );
1534
+ const gender2 = extractGender(
1535
+ person2Data?.fullDetails?.persons?.[0] || person2Data
1536
+ );
1537
+ const p1 = personIdMap.get(person1);
1538
+ const p2 = personIdMap.get(person2);
1539
+ if (gender1 === "M" || gender2 === "F") {
1540
+ if (p1) lines.push(`1 HUSB ${p1}`);
1541
+ if (p2) lines.push(`1 WIFE ${p2}`);
1542
+ } else {
1543
+ if (p2) lines.push(`1 HUSB ${p2}`);
1544
+ if (p1) lines.push(`1 WIFE ${p1}`);
1545
+ }
1546
+ addMarriageFacts(lines, allRelationships, person1, person2);
1547
+ } else if (spouseArray.length === 1) {
1548
+ const parentData = pedigreeData.persons?.find(
1549
+ (p) => p.id === spouseArray[0]
1550
+ );
1551
+ const gender = extractGender(
1552
+ parentData?.fullDetails?.persons?.[0] || parentData
1553
+ );
1554
+ if (gender === "M") {
1555
+ lines.push(`1 HUSB ${personIdMap.get(spouseArray[0])}`);
1556
+ } else {
1557
+ lines.push(`1 WIFE ${personIdMap.get(spouseArray[0])}`);
1558
+ }
1559
+ }
1560
+ family.children.forEach((childId) => {
1561
+ const childGedcomId = personIdMap.get(childId);
1562
+ if (childGedcomId) {
1563
+ lines.push(`1 CHIL ${childGedcomId}`);
1564
+ }
1565
+ });
1566
+ });
1567
+ childToParents.forEach((parents, childId) => {
1568
+ const childGedcomId = personIdMap.get(childId);
1569
+ if (!childGedcomId) return;
1570
+ let famId;
1571
+ if (parents.length >= 2) {
1572
+ const famKey = parents.slice(0, 2).sort().join("-");
1573
+ famId = familyIdMap.get(famKey);
1574
+ } else if (parents.length === 1) {
1575
+ const famKey = `single-${parents[0]}`;
1576
+ famId = familyIdMap.get(famKey);
1577
+ }
1578
+ if (famId) {
1579
+ const indiRecordIndex = lines.findIndex(
1580
+ (line) => line === `0 ${childGedcomId} INDI`
1581
+ );
1582
+ if (indiRecordIndex !== -1) {
1583
+ lines.splice(indiRecordIndex + 1, 0, `1 FAMC ${famId}`);
1584
+ }
1585
+ }
1586
+ });
1587
+ families.forEach((family, key) => {
1588
+ const famId = familyIdMap.get(key);
1589
+ if (!famId) return;
1590
+ family.spouses.forEach((spouseId) => {
1591
+ const spouseGedcomId = personIdMap.get(spouseId);
1592
+ if (!spouseGedcomId) return;
1593
+ const indiRecordIndex = lines.findIndex(
1594
+ (line) => line === `0 ${spouseGedcomId} INDI`
1595
+ );
1596
+ if (indiRecordIndex !== -1) {
1597
+ let insertIndex = indiRecordIndex + 1;
1598
+ while (insertIndex < lines.length && lines[insertIndex].startsWith("1 FAMC")) {
1599
+ insertIndex++;
1600
+ }
1601
+ lines.splice(insertIndex, 0, `1 FAMS ${famId}`);
1602
+ }
1603
+ });
1604
+ });
1605
+ const personsInFamilies = /* @__PURE__ */ new Set();
1606
+ families.forEach((family) => {
1607
+ family.spouses.forEach((id) => personsInFamilies.add(id));
1608
+ family.children.forEach((id) => personsInFamilies.add(id));
1609
+ });
1610
+ lines.push("0 TRLR");
1611
+ return lines.join("\n");
1612
+ }
1613
+ function addMarriageFacts(lines, relationships, person1, person2) {
1614
+ const relKey = [person1, person2].sort().join("-");
1615
+ const rel = relationships.find((r) => {
1616
+ const a = r.person1?.resourceId || r.person1?.resource?.resourceId;
1617
+ const b = r.person2?.resourceId || r.person2?.resource?.resourceId;
1618
+ if (!a || !b) return false;
1619
+ return [a, b].sort().join("-") === relKey;
1620
+ });
1621
+ if (!rel) return;
1622
+ const marriageFacts = [];
1623
+ if (rel.facts && Array.isArray(rel.facts)) {
1624
+ rel.facts.forEach((f) => {
1625
+ if (f.type?.includes("Marriage")) {
1626
+ marriageFacts.push(f);
1627
+ }
1628
+ });
1629
+ }
1630
+ if (rel.details?.facts && Array.isArray(rel.details.facts)) {
1631
+ marriageFacts.push(
1632
+ ...rel.details.facts.filter((f) => f.type?.includes("Marriage"))
1633
+ );
1634
+ }
1635
+ if (rel.details?.persons && Array.isArray(rel.details.persons)) {
1636
+ rel.details.persons.forEach((p) => {
1637
+ if (p.facts && Array.isArray(p.facts)) {
1638
+ p.facts.forEach((f) => {
1639
+ if (f.type?.includes("Marriage")) {
1640
+ marriageFacts.push(f);
1641
+ }
1642
+ });
1643
+ }
1644
+ });
1645
+ }
1646
+ marriageFacts.forEach((mf) => {
1647
+ const factLines = convertFactToGedcom(mf);
1648
+ factLines.forEach((line) => lines.push(line));
1649
+ });
1650
+ }
1651
+ var convertFamilySearchToGedcom = convertToGedcom;
1652
+
1653
+ exports.ENVIRONMENT_CONFIGS = ENVIRONMENT_CONFIGS;
1654
+ exports.FamilySearchSDK = FamilySearchSDK;
1655
+ exports.OAUTH_ENDPOINTS = OAUTH_ENDPOINTS;
1656
+ exports.OAUTH_STORAGE_KEYS = OAUTH_STORAGE_KEYS;
1657
+ exports.buildAuthorizationUrl = buildAuthorizationUrl;
1658
+ exports.clearAllTokens = clearAllTokens;
1659
+ exports.clearStoredTokens = clearStoredTokens;
1660
+ exports.convertFamilySearchToGedcom = convertFamilySearchToGedcom;
1661
+ exports.convertToGedcom = convertToGedcom;
1662
+ exports.createFamilySearchSDK = createFamilySearchSDK;
1663
+ exports.exchangeCodeForToken = exchangeCodeForToken;
1664
+ exports.fetchMultiplePersons = fetchMultiplePersons;
1665
+ exports.fetchPedigree = fetchPedigree;
1666
+ exports.generateOAuthState = generateOAuthState;
1667
+ exports.getCurrentUser = getCurrentUser;
1668
+ exports.getFamilySearchSDK = getFamilySearchSDK;
1669
+ exports.getOAuthEndpoints = getOAuthEndpoints;
1670
+ exports.getPersonWithDetails = getPersonWithDetails;
1671
+ exports.getPlaceById = getPlaceById;
1672
+ exports.getPlaceChildren = getPlaceChildren;
1673
+ exports.getPlaceDetails = getPlaceDetails;
1674
+ exports.getStoredAccessToken = getStoredAccessToken;
1675
+ exports.getStoredRefreshToken = getStoredRefreshToken;
1676
+ exports.getTokenStorageKey = getTokenStorageKey;
1677
+ exports.getUserInfo = getUserInfo;
1678
+ exports.initFamilySearchSDK = initFamilySearchSDK;
1679
+ exports.openOAuthPopup = openOAuthPopup;
1680
+ exports.parseCallbackParams = parseCallbackParams;
1681
+ exports.refreshAccessToken = refreshAccessToken;
1682
+ exports.resetFamilySearchSDK = resetFamilySearchSDK;
1683
+ exports.searchPlaces = searchPlaces;
1684
+ exports.storeOAuthState = storeOAuthState;
1685
+ exports.storeTokens = storeTokens;
1686
+ exports.validateAccessToken = validateAccessToken;
1687
+ exports.validateOAuthState = validateOAuthState;
1688
+ //# sourceMappingURL=index.cjs.map
1689
+ //# sourceMappingURL=index.cjs.map