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