@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/LICENSE +21 -0
- package/README.md +227 -0
- package/dist/auth/index.cjs +313 -0
- package/dist/auth/index.cjs.map +1 -0
- package/dist/auth/index.d.cts +124 -0
- package/dist/auth/index.d.ts +124 -0
- package/dist/auth/index.js +293 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/client-DIpYSHtx.d.ts +162 -0
- package/dist/client-ohjqX4t5.d.cts +162 -0
- package/dist/index-D6H-lvis.d.cts +484 -0
- package/dist/index-D6H-lvis.d.ts +484 -0
- package/dist/index.cjs +1689 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1653 -0
- package/dist/index.js.map +1 -0
- package/dist/places/index.cjs +94 -0
- package/dist/places/index.cjs.map +1 -0
- package/dist/places/index.d.cts +69 -0
- package/dist/places/index.d.ts +69 -0
- package/dist/places/index.js +89 -0
- package/dist/places/index.js.map +1 -0
- package/dist/tree/index.cjs +191 -0
- package/dist/tree/index.cjs.map +1 -0
- package/dist/tree/index.d.cts +47 -0
- package/dist/tree/index.d.ts +47 -0
- package/dist/tree/index.js +186 -0
- package/dist/tree/index.js.map +1 -0
- package/dist/utils/index.cjs +663 -0
- package/dist/utils/index.cjs.map +1 -0
- package/dist/utils/index.d.cts +46 -0
- package/dist/utils/index.d.ts +46 -0
- package/dist/utils/index.js +660 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +78 -0
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
|