@eightyfourthousand/data-access 2026.3.0 → 2026.4.0
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/README.md +4 -2
- package/package.json +2 -2
- package/src/index.ts +1 -2
- package/src/lib/folio.ts +23 -9
- package/src/lib/glossary/batch.ts +35 -0
- package/src/lib/glossary/index.ts +4 -0
- package/src/lib/glossary/instance.ts +54 -0
- package/src/lib/glossary/landing.ts +50 -0
- package/src/lib/glossary/pagination.ts +471 -0
- package/src/lib/passage/batch.ts +229 -0
- package/src/lib/passage/index.ts +5 -0
- package/src/lib/passage/pagination.ts +277 -0
- package/src/lib/passage/read.ts +100 -0
- package/src/lib/passage/replace-persistence.ts +153 -0
- package/src/lib/passage/save.ts +324 -0
- package/src/lib/publications.ts +168 -0
- package/src/lib/replace.spec.ts +154 -0
- package/src/lib/replace.ts +244 -0
- package/src/lib/types/folio.ts +2 -2
- package/src/lib/types/glossary-page.ts +0 -118
- package/src/lib/types/glossary.ts +3 -2
- package/src/lib/types/index.ts +0 -3
- package/src/lib/canon.ts +0 -111
- package/src/lib/glossary.ts +0 -147
- package/src/lib/passage.ts +0 -122
- package/src/lib/projects.ts +0 -107
- package/src/lib/types/canon.ts +0 -148
- package/src/lib/types/contributor.ts +0 -84
- package/src/lib/types/project.ts +0 -200
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import { DataClient, Passage, PassageDTO, passageFromDTO } from '../types';
|
|
2
|
+
|
|
3
|
+
type ApiPaginationDirection = 'FORWARD' | 'BACKWARD' | 'AROUND';
|
|
4
|
+
|
|
5
|
+
type GlossaryTermNode = {
|
|
6
|
+
uuid: string;
|
|
7
|
+
authority: string;
|
|
8
|
+
definition: string | null;
|
|
9
|
+
termNumber: number;
|
|
10
|
+
names: {
|
|
11
|
+
english: string | null;
|
|
12
|
+
tibetan: string | null;
|
|
13
|
+
sanskrit: string | null;
|
|
14
|
+
pali: string | null;
|
|
15
|
+
chinese: string | null;
|
|
16
|
+
wylie: string | null;
|
|
17
|
+
alternatives: string | null;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type GlossaryTermIndexRow = {
|
|
22
|
+
glossary_uuid: string;
|
|
23
|
+
authority_uuid: string;
|
|
24
|
+
term_number: number | string;
|
|
25
|
+
definition: string | null;
|
|
26
|
+
english: string | null;
|
|
27
|
+
wylie: string | null;
|
|
28
|
+
tibetan: string | null;
|
|
29
|
+
sanskrit_plain: string | null;
|
|
30
|
+
sanskrit_attested: string | null;
|
|
31
|
+
chinese: string | null;
|
|
32
|
+
pali: string | null;
|
|
33
|
+
alternatives: string | null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type GlossaryPageInfo = {
|
|
37
|
+
nextCursor: string | null;
|
|
38
|
+
prevCursor: string | null;
|
|
39
|
+
hasMoreAfter: boolean;
|
|
40
|
+
hasMoreBefore: boolean;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type GlossaryTermConnection = {
|
|
44
|
+
nodes: GlossaryTermNode[];
|
|
45
|
+
pageInfo: GlossaryPageInfo;
|
|
46
|
+
totalCount: number;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type GlossaryPassagesPage = {
|
|
50
|
+
items: Passage[];
|
|
51
|
+
nextCursor: string | null;
|
|
52
|
+
hasMore: boolean;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const DEFAULT_GLOSSARY_LIMIT = 50;
|
|
56
|
+
const MAX_GLOSSARY_LIMIT = 200;
|
|
57
|
+
const DEFAULT_GLOSSARY_PASSAGES_LIMIT = 10;
|
|
58
|
+
|
|
59
|
+
function buildGlossaryTermConnection(
|
|
60
|
+
nodes: GlossaryTermNode[],
|
|
61
|
+
nextCursor: string | null,
|
|
62
|
+
prevCursor: string | null,
|
|
63
|
+
hasMoreAfter: boolean,
|
|
64
|
+
hasMoreBefore: boolean,
|
|
65
|
+
totalCount: number,
|
|
66
|
+
): GlossaryTermConnection {
|
|
67
|
+
return {
|
|
68
|
+
nodes,
|
|
69
|
+
pageInfo: {
|
|
70
|
+
nextCursor,
|
|
71
|
+
prevCursor,
|
|
72
|
+
hasMoreAfter,
|
|
73
|
+
hasMoreBefore,
|
|
74
|
+
},
|
|
75
|
+
totalCount,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseOffsetCursor(after?: string) {
|
|
80
|
+
if (!after) return 0;
|
|
81
|
+
|
|
82
|
+
const parsed = Number.parseInt(after, 10);
|
|
83
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseCount(value: number | string | null | undefined) {
|
|
87
|
+
if (typeof value === 'number') return value;
|
|
88
|
+
|
|
89
|
+
const parsed = Number.parseInt(value ?? '', 10);
|
|
90
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function rowToGlossaryTermNode(
|
|
94
|
+
row: Pick<
|
|
95
|
+
GlossaryTermIndexRow,
|
|
96
|
+
| 'glossary_uuid'
|
|
97
|
+
| 'authority_uuid'
|
|
98
|
+
| 'definition'
|
|
99
|
+
| 'term_number'
|
|
100
|
+
| 'english'
|
|
101
|
+
| 'wylie'
|
|
102
|
+
| 'tibetan'
|
|
103
|
+
| 'sanskrit_plain'
|
|
104
|
+
| 'sanskrit_attested'
|
|
105
|
+
| 'chinese'
|
|
106
|
+
| 'pali'
|
|
107
|
+
| 'alternatives'
|
|
108
|
+
> & { withAttestations: boolean },
|
|
109
|
+
): GlossaryTermNode {
|
|
110
|
+
return {
|
|
111
|
+
uuid: row.glossary_uuid,
|
|
112
|
+
authority: row.authority_uuid,
|
|
113
|
+
definition: row.definition,
|
|
114
|
+
termNumber: parseCount(row.term_number),
|
|
115
|
+
names: {
|
|
116
|
+
english: row.english,
|
|
117
|
+
alternatives: row.alternatives,
|
|
118
|
+
wylie: row.wylie,
|
|
119
|
+
tibetan: row.tibetan,
|
|
120
|
+
sanskrit: row.withAttestations
|
|
121
|
+
? row.sanskrit_attested
|
|
122
|
+
: row.sanskrit_plain,
|
|
123
|
+
chinese: row.chinese,
|
|
124
|
+
pali: row.pali,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const getGlossaryTermPassagesPage = async ({
|
|
130
|
+
client,
|
|
131
|
+
uuid,
|
|
132
|
+
first,
|
|
133
|
+
after,
|
|
134
|
+
}: {
|
|
135
|
+
client: DataClient;
|
|
136
|
+
uuid: string;
|
|
137
|
+
first?: number;
|
|
138
|
+
after?: string;
|
|
139
|
+
}): Promise<GlossaryPassagesPage> => {
|
|
140
|
+
const limit = Math.max(first ?? DEFAULT_GLOSSARY_PASSAGES_LIMIT, 1);
|
|
141
|
+
const offset = parseOffsetCursor(after);
|
|
142
|
+
|
|
143
|
+
const { data: annotations, error: annotationsError } = await client
|
|
144
|
+
.from('passage_annotations')
|
|
145
|
+
.select('passage_uuid')
|
|
146
|
+
.eq('type', 'glossary-instance')
|
|
147
|
+
.filter('content', 'cs', JSON.stringify([{ uuid }]));
|
|
148
|
+
|
|
149
|
+
if (annotationsError) {
|
|
150
|
+
console.error(
|
|
151
|
+
'Error fetching glossary passage annotations:',
|
|
152
|
+
annotationsError,
|
|
153
|
+
);
|
|
154
|
+
return { items: [], nextCursor: null, hasMore: false };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const passageUuids = (annotations ?? []).map(
|
|
158
|
+
(annotation: { passage_uuid: string }) => annotation.passage_uuid,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
if (passageUuids.length === 0) {
|
|
162
|
+
return { items: [], nextCursor: null, hasMore: false };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const { data: passages, error: passagesError } = await client
|
|
166
|
+
.from('passages')
|
|
167
|
+
.select('uuid, content, label, sort, type, xmlId, work_uuid, toh')
|
|
168
|
+
.in('uuid', passageUuids)
|
|
169
|
+
.order('sort', { ascending: true })
|
|
170
|
+
.range(offset, offset + limit);
|
|
171
|
+
|
|
172
|
+
if (passagesError) {
|
|
173
|
+
console.error('Error fetching glossary passages:', passagesError);
|
|
174
|
+
return { items: [], nextCursor: null, hasMore: false };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const rows = passages ?? [];
|
|
178
|
+
const hasMore = rows.length > limit;
|
|
179
|
+
const items = hasMore ? rows.slice(0, limit) : rows;
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
items: items.map((passage: PassageDTO) => passageFromDTO(passage)),
|
|
183
|
+
nextCursor: hasMore ? String(offset + limit) : null,
|
|
184
|
+
hasMore,
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export const getWorkGlossaryTermsPage = async ({
|
|
189
|
+
client,
|
|
190
|
+
workUuid,
|
|
191
|
+
limit = DEFAULT_GLOSSARY_LIMIT,
|
|
192
|
+
cursor,
|
|
193
|
+
direction = 'FORWARD',
|
|
194
|
+
withAttestations = false,
|
|
195
|
+
}: {
|
|
196
|
+
client: DataClient;
|
|
197
|
+
workUuid: string;
|
|
198
|
+
limit?: number;
|
|
199
|
+
cursor?: string | null;
|
|
200
|
+
direction?: ApiPaginationDirection;
|
|
201
|
+
withAttestations?: boolean;
|
|
202
|
+
}): Promise<GlossaryTermConnection> => {
|
|
203
|
+
const clampedLimit = Math.min(Math.max(limit, 1), MAX_GLOSSARY_LIMIT);
|
|
204
|
+
|
|
205
|
+
if (direction === 'AROUND') {
|
|
206
|
+
return getWorkGlossaryTermsAround({
|
|
207
|
+
client,
|
|
208
|
+
workUuid,
|
|
209
|
+
limit: clampedLimit,
|
|
210
|
+
cursor,
|
|
211
|
+
withAttestations,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const [
|
|
216
|
+
{ count, error: countError },
|
|
217
|
+
{ data: cursorRows, error: cursorError },
|
|
218
|
+
] = await Promise.all([
|
|
219
|
+
client
|
|
220
|
+
.from('glossary_term_index')
|
|
221
|
+
.select('authority_uuid', { count: 'exact', head: true })
|
|
222
|
+
.eq('work_uuid', workUuid),
|
|
223
|
+
cursor
|
|
224
|
+
? client
|
|
225
|
+
.from('glossary_term_index')
|
|
226
|
+
.select('authority_uuid, term_number')
|
|
227
|
+
.eq('work_uuid', workUuid)
|
|
228
|
+
.eq('authority_uuid', cursor)
|
|
229
|
+
.limit(1)
|
|
230
|
+
: Promise.resolve({ data: [], error: null }),
|
|
231
|
+
]);
|
|
232
|
+
|
|
233
|
+
if (countError) {
|
|
234
|
+
console.error('Error counting glossary terms:', countError);
|
|
235
|
+
return buildGlossaryTermConnection([], null, null, false, false, 0);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (cursorError) {
|
|
239
|
+
console.error('Error fetching glossary cursor term:', cursorError);
|
|
240
|
+
return buildGlossaryTermConnection([], null, null, false, false, 0);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const totalCount = count ?? 0;
|
|
244
|
+
if (totalCount === 0) {
|
|
245
|
+
return buildGlossaryTermConnection([], null, null, false, false, 0);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const cursorRow = cursor
|
|
249
|
+
? ((cursorRows ?? [])[0] as
|
|
250
|
+
| { authority_uuid: string; term_number: number | string }
|
|
251
|
+
| undefined)
|
|
252
|
+
: undefined;
|
|
253
|
+
|
|
254
|
+
if (cursor && !cursorRow) {
|
|
255
|
+
return buildGlossaryTermConnection(
|
|
256
|
+
[],
|
|
257
|
+
null,
|
|
258
|
+
null,
|
|
259
|
+
false,
|
|
260
|
+
false,
|
|
261
|
+
totalCount,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const cursorTermNumber =
|
|
266
|
+
cursorRow !== undefined ? parseCount(cursorRow.term_number) : null;
|
|
267
|
+
let query = client
|
|
268
|
+
.from('glossary_term_index')
|
|
269
|
+
.select(
|
|
270
|
+
`glossary_uuid,
|
|
271
|
+
authority_uuid,
|
|
272
|
+
term_number,
|
|
273
|
+
definition,
|
|
274
|
+
english,
|
|
275
|
+
wylie,
|
|
276
|
+
tibetan,
|
|
277
|
+
sanskrit_plain,
|
|
278
|
+
sanskrit_attested,
|
|
279
|
+
chinese,
|
|
280
|
+
pali,
|
|
281
|
+
alternatives`,
|
|
282
|
+
)
|
|
283
|
+
.eq('work_uuid', workUuid);
|
|
284
|
+
|
|
285
|
+
if (cursorTermNumber !== null) {
|
|
286
|
+
query =
|
|
287
|
+
direction === 'FORWARD'
|
|
288
|
+
? query.gt('term_number', cursorTermNumber)
|
|
289
|
+
: query.lt('term_number', cursorTermNumber);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const ascending = direction === 'FORWARD';
|
|
293
|
+
const { data, error } = await query
|
|
294
|
+
.order('term_number', { ascending })
|
|
295
|
+
.limit(clampedLimit);
|
|
296
|
+
|
|
297
|
+
if (error) {
|
|
298
|
+
console.error('Error fetching paginated glossary terms:', error);
|
|
299
|
+
return buildGlossaryTermConnection(
|
|
300
|
+
[],
|
|
301
|
+
null,
|
|
302
|
+
null,
|
|
303
|
+
false,
|
|
304
|
+
false,
|
|
305
|
+
totalCount,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const pageRows = (data ?? []) as GlossaryTermIndexRow[];
|
|
310
|
+
const rows = ascending ? pageRows : [...pageRows].reverse();
|
|
311
|
+
if (rows.length === 0) {
|
|
312
|
+
return buildGlossaryTermConnection(
|
|
313
|
+
[],
|
|
314
|
+
null,
|
|
315
|
+
null,
|
|
316
|
+
false,
|
|
317
|
+
false,
|
|
318
|
+
totalCount,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const nodes = rows.map((row) =>
|
|
323
|
+
rowToGlossaryTermNode({ ...row, withAttestations }),
|
|
324
|
+
);
|
|
325
|
+
const firstRow = rows[0];
|
|
326
|
+
const lastRow = rows[rows.length - 1];
|
|
327
|
+
const hasMoreBefore = parseCount(firstRow.term_number) > 1;
|
|
328
|
+
const hasMoreAfter = parseCount(lastRow.term_number) < totalCount;
|
|
329
|
+
|
|
330
|
+
return buildGlossaryTermConnection(
|
|
331
|
+
nodes,
|
|
332
|
+
hasMoreAfter ? lastRow.authority_uuid : null,
|
|
333
|
+
hasMoreBefore ? firstRow.authority_uuid : null,
|
|
334
|
+
hasMoreAfter,
|
|
335
|
+
hasMoreBefore,
|
|
336
|
+
totalCount,
|
|
337
|
+
);
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
export const getWorkGlossaryTermsAround = async ({
|
|
341
|
+
client,
|
|
342
|
+
workUuid,
|
|
343
|
+
limit,
|
|
344
|
+
cursor,
|
|
345
|
+
withAttestations,
|
|
346
|
+
}: {
|
|
347
|
+
client: DataClient;
|
|
348
|
+
workUuid: string;
|
|
349
|
+
limit: number;
|
|
350
|
+
cursor?: string | null;
|
|
351
|
+
withAttestations: boolean;
|
|
352
|
+
}): Promise<GlossaryTermConnection> => {
|
|
353
|
+
if (!cursor) {
|
|
354
|
+
return getWorkGlossaryTermsPage({
|
|
355
|
+
client,
|
|
356
|
+
workUuid,
|
|
357
|
+
limit,
|
|
358
|
+
cursor: null,
|
|
359
|
+
direction: 'FORWARD',
|
|
360
|
+
withAttestations,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const [
|
|
365
|
+
{ count, error: countError },
|
|
366
|
+
{ data: cursorRows, error: cursorError },
|
|
367
|
+
] = await Promise.all([
|
|
368
|
+
client
|
|
369
|
+
.from('glossary_term_index')
|
|
370
|
+
.select('authority_uuid', { count: 'exact', head: true })
|
|
371
|
+
.eq('work_uuid', workUuid),
|
|
372
|
+
client
|
|
373
|
+
.from('glossary_term_index')
|
|
374
|
+
.select('authority_uuid, term_number')
|
|
375
|
+
.eq('work_uuid', workUuid)
|
|
376
|
+
.eq('authority_uuid', cursor)
|
|
377
|
+
.limit(1),
|
|
378
|
+
]);
|
|
379
|
+
|
|
380
|
+
if (countError) {
|
|
381
|
+
console.error('Error counting glossary terms:', countError);
|
|
382
|
+
return buildGlossaryTermConnection([], null, null, false, false, 0);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (cursorError) {
|
|
386
|
+
console.error('Error fetching glossary cursor term:', cursorError);
|
|
387
|
+
return getWorkGlossaryTermsPage({
|
|
388
|
+
client,
|
|
389
|
+
workUuid,
|
|
390
|
+
limit,
|
|
391
|
+
cursor: null,
|
|
392
|
+
direction: 'FORWARD',
|
|
393
|
+
withAttestations,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const totalCount = count ?? 0;
|
|
398
|
+
const cursorRow = (cursorRows ?? [])[0] as
|
|
399
|
+
| { authority_uuid: string; term_number: number | string }
|
|
400
|
+
| undefined;
|
|
401
|
+
|
|
402
|
+
if (!cursorRow) {
|
|
403
|
+
return getWorkGlossaryTermsPage({
|
|
404
|
+
client,
|
|
405
|
+
workUuid,
|
|
406
|
+
limit,
|
|
407
|
+
cursor: null,
|
|
408
|
+
direction: 'FORWARD',
|
|
409
|
+
withAttestations,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const cursorTermNumber = parseCount(cursorRow.term_number);
|
|
414
|
+
let startTerm = Math.max(1, cursorTermNumber - Math.floor(limit / 2));
|
|
415
|
+
let endTerm = startTerm + limit - 1;
|
|
416
|
+
|
|
417
|
+
if (endTerm > totalCount) {
|
|
418
|
+
endTerm = totalCount;
|
|
419
|
+
startTerm = Math.max(1, endTerm - limit + 1);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const { data, error } = await client
|
|
423
|
+
.from('glossary_term_index')
|
|
424
|
+
.select(
|
|
425
|
+
`glossary_uuid,
|
|
426
|
+
authority_uuid,
|
|
427
|
+
term_number,
|
|
428
|
+
definition,
|
|
429
|
+
english,
|
|
430
|
+
wylie,
|
|
431
|
+
tibetan,
|
|
432
|
+
sanskrit_plain,
|
|
433
|
+
sanskrit_attested,
|
|
434
|
+
chinese,
|
|
435
|
+
pali`,
|
|
436
|
+
)
|
|
437
|
+
.eq('work_uuid', workUuid)
|
|
438
|
+
.gte('term_number', startTerm)
|
|
439
|
+
.lte('term_number', endTerm)
|
|
440
|
+
.order('term_number', { ascending: true });
|
|
441
|
+
|
|
442
|
+
if (error) {
|
|
443
|
+
console.error('Error fetching glossary terms around cursor:', error);
|
|
444
|
+
return buildGlossaryTermConnection(
|
|
445
|
+
[],
|
|
446
|
+
null,
|
|
447
|
+
null,
|
|
448
|
+
false,
|
|
449
|
+
false,
|
|
450
|
+
totalCount,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const rows = (data ?? []) as GlossaryTermIndexRow[];
|
|
455
|
+
const nodes = rows.map((row) =>
|
|
456
|
+
rowToGlossaryTermNode({ ...row, withAttestations }),
|
|
457
|
+
);
|
|
458
|
+
const hasMoreBefore = startTerm > 1;
|
|
459
|
+
const hasMoreAfter = endTerm < totalCount;
|
|
460
|
+
const firstRow = rows[0];
|
|
461
|
+
const lastRow = rows[rows.length - 1];
|
|
462
|
+
|
|
463
|
+
return buildGlossaryTermConnection(
|
|
464
|
+
nodes,
|
|
465
|
+
hasMoreAfter && lastRow ? lastRow.authority_uuid : null,
|
|
466
|
+
hasMoreBefore && firstRow ? firstRow.authority_uuid : null,
|
|
467
|
+
hasMoreAfter,
|
|
468
|
+
hasMoreBefore,
|
|
469
|
+
totalCount,
|
|
470
|
+
);
|
|
471
|
+
};
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AlignmentDTO,
|
|
3
|
+
AnnotationDTO,
|
|
4
|
+
DataClient,
|
|
5
|
+
Passage,
|
|
6
|
+
PassageDTO,
|
|
7
|
+
Passages,
|
|
8
|
+
passageFromDTO,
|
|
9
|
+
} from '../types';
|
|
10
|
+
|
|
11
|
+
export const getAnnotationsByPassageUuids = async ({
|
|
12
|
+
client,
|
|
13
|
+
passageUuids,
|
|
14
|
+
}: {
|
|
15
|
+
client: DataClient;
|
|
16
|
+
passageUuids: readonly string[];
|
|
17
|
+
}): Promise<Map<string, AnnotationDTO[]>> => {
|
|
18
|
+
const annotationsByPassage = new Map<string, AnnotationDTO[]>();
|
|
19
|
+
|
|
20
|
+
if (passageUuids.length === 0) {
|
|
21
|
+
return annotationsByPassage;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const pageSize = 1000;
|
|
25
|
+
let allData: AnnotationDTO[] = [];
|
|
26
|
+
let offset = 0;
|
|
27
|
+
let hasMore = true;
|
|
28
|
+
|
|
29
|
+
while (hasMore) {
|
|
30
|
+
const { data, error } = await client
|
|
31
|
+
.from('passage_annotations')
|
|
32
|
+
.select('uuid, passage_uuid, type, start, end, content, toh')
|
|
33
|
+
.in('passage_uuid', passageUuids as string[])
|
|
34
|
+
.not('type', 'like', 'deprecated%')
|
|
35
|
+
.range(offset, offset + pageSize - 1);
|
|
36
|
+
|
|
37
|
+
if (error) {
|
|
38
|
+
console.error('Error batch loading annotations:', error);
|
|
39
|
+
return new Map();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
allData = allData.concat((data ?? []) as AnnotationDTO[]);
|
|
43
|
+
hasMore = (data?.length ?? 0) === pageSize;
|
|
44
|
+
offset += pageSize;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const annotation of allData) {
|
|
48
|
+
const passageUuid = annotation.passage_uuid;
|
|
49
|
+
if (!passageUuid) continue;
|
|
50
|
+
const existing = annotationsByPassage.get(passageUuid);
|
|
51
|
+
if (existing) {
|
|
52
|
+
existing.push(annotation);
|
|
53
|
+
} else {
|
|
54
|
+
annotationsByPassage.set(passageUuid, [annotation]);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const annotations of annotationsByPassage.values()) {
|
|
59
|
+
annotations.sort((a, b) => {
|
|
60
|
+
if (a.start !== b.start) return a.start - b.start;
|
|
61
|
+
return b.end - a.end;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return annotationsByPassage;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const getAlignmentsByPassageUuids = async ({
|
|
69
|
+
client,
|
|
70
|
+
passageUuids,
|
|
71
|
+
}: {
|
|
72
|
+
client: DataClient;
|
|
73
|
+
passageUuids: readonly string[];
|
|
74
|
+
}): Promise<Map<string, AlignmentDTO[]>> => {
|
|
75
|
+
const alignmentsByPassage = new Map<string, AlignmentDTO[]>();
|
|
76
|
+
|
|
77
|
+
if (passageUuids.length === 0) {
|
|
78
|
+
return alignmentsByPassage;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const pageSize = 1000;
|
|
82
|
+
let allData: (AlignmentDTO & { passage_uuid: string })[] = [];
|
|
83
|
+
let offset = 0;
|
|
84
|
+
let hasMore = true;
|
|
85
|
+
|
|
86
|
+
while (hasMore) {
|
|
87
|
+
const { data, error } = await client
|
|
88
|
+
.from('passage_alignments')
|
|
89
|
+
.select(
|
|
90
|
+
'passage_uuid, folio_uuid, toh, tibetan, folio_number, volume_number',
|
|
91
|
+
)
|
|
92
|
+
.in('passage_uuid', passageUuids as string[])
|
|
93
|
+
.range(offset, offset + pageSize - 1);
|
|
94
|
+
|
|
95
|
+
if (error) {
|
|
96
|
+
console.error('Error batch loading alignments:', error);
|
|
97
|
+
return new Map();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
allData = allData.concat((data ?? []) as AlignmentDTO[]);
|
|
101
|
+
hasMore = (data?.length ?? 0) === pageSize;
|
|
102
|
+
offset += pageSize;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const row of allData) {
|
|
106
|
+
const existing = alignmentsByPassage.get(row.passage_uuid);
|
|
107
|
+
if (existing) {
|
|
108
|
+
existing.push(row);
|
|
109
|
+
} else {
|
|
110
|
+
alignmentsByPassage.set(row.passage_uuid, [row]);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return alignmentsByPassage;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const getPassageLabelsByUuids = async ({
|
|
118
|
+
client,
|
|
119
|
+
passageUuids,
|
|
120
|
+
}: {
|
|
121
|
+
client: DataClient;
|
|
122
|
+
passageUuids: readonly string[];
|
|
123
|
+
}): Promise<Map<string, string>> => {
|
|
124
|
+
const labelsByUuid = new Map<string, string>();
|
|
125
|
+
|
|
126
|
+
if (passageUuids.length === 0) {
|
|
127
|
+
return labelsByUuid;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const { data, error } = await client
|
|
131
|
+
.from('passages')
|
|
132
|
+
.select('uuid, label')
|
|
133
|
+
.in('uuid', passageUuids as string[]);
|
|
134
|
+
|
|
135
|
+
if (error) {
|
|
136
|
+
console.error('Error batch loading passage labels:', error);
|
|
137
|
+
return labelsByUuid;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const passage of data ?? []) {
|
|
141
|
+
if (passage.label) {
|
|
142
|
+
labelsByUuid.set(passage.uuid, passage.label);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return labelsByUuid;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const getPassageReferencesByTargetUuids = async ({
|
|
150
|
+
client,
|
|
151
|
+
passageUuids,
|
|
152
|
+
}: {
|
|
153
|
+
client: DataClient;
|
|
154
|
+
passageUuids: readonly string[];
|
|
155
|
+
}): Promise<Map<string, Passages>> => {
|
|
156
|
+
const referencesByTargetUuid = new Map<string, Passages>();
|
|
157
|
+
|
|
158
|
+
if (passageUuids.length === 0) {
|
|
159
|
+
return referencesByTargetUuid;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const { data: annotations, error: annotationsError } = await client.rpc(
|
|
163
|
+
'get_passage_annotations_by_content_uuids',
|
|
164
|
+
{
|
|
165
|
+
annotation_type: 'end-note-link',
|
|
166
|
+
target_uuids: passageUuids as string[],
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
if (annotationsError) {
|
|
171
|
+
console.error(
|
|
172
|
+
'Error batch loading passage reference annotations:',
|
|
173
|
+
annotationsError,
|
|
174
|
+
);
|
|
175
|
+
return referencesByTargetUuid;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const targetToSourceUuids = new Map<string, Set<string>>();
|
|
179
|
+
const allSourceUuids = new Set<string>();
|
|
180
|
+
|
|
181
|
+
for (const row of (annotations ?? []) as Array<{
|
|
182
|
+
passage_uuid: string;
|
|
183
|
+
target_uuid: string;
|
|
184
|
+
}>) {
|
|
185
|
+
if (!row.target_uuid || !row.passage_uuid) continue;
|
|
186
|
+
allSourceUuids.add(row.passage_uuid);
|
|
187
|
+
const sourceSet =
|
|
188
|
+
targetToSourceUuids.get(row.target_uuid) ?? new Set<string>();
|
|
189
|
+
sourceSet.add(row.passage_uuid);
|
|
190
|
+
targetToSourceUuids.set(row.target_uuid, sourceSet);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const sourceUuidArray = Array.from(allSourceUuids);
|
|
194
|
+
const passageMap = new Map<string, Passage>();
|
|
195
|
+
const inBatchSize = 300;
|
|
196
|
+
|
|
197
|
+
for (let i = 0; i < sourceUuidArray.length; i += inBatchSize) {
|
|
198
|
+
const batch = sourceUuidArray.slice(i, i + inBatchSize);
|
|
199
|
+
const { data, error } = await client
|
|
200
|
+
.from('passages')
|
|
201
|
+
.select('uuid, content, label, sort, type, toh, xmlId, work_uuid')
|
|
202
|
+
.in('uuid', batch);
|
|
203
|
+
|
|
204
|
+
if (error) {
|
|
205
|
+
console.error('Error batch loading passage reference data:', error);
|
|
206
|
+
return new Map();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const row of (data ?? []) as PassageDTO[]) {
|
|
210
|
+
passageMap.set(row.uuid, passageFromDTO(row));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (const targetUuid of passageUuids) {
|
|
215
|
+
const sourceUuids = targetToSourceUuids.get(targetUuid);
|
|
216
|
+
if (!sourceUuids) continue;
|
|
217
|
+
const references: Passages = [];
|
|
218
|
+
for (const sourceUuid of sourceUuids) {
|
|
219
|
+
const passage = passageMap.get(sourceUuid);
|
|
220
|
+
if (passage) references.push(passage);
|
|
221
|
+
}
|
|
222
|
+
referencesByTargetUuid.set(
|
|
223
|
+
targetUuid,
|
|
224
|
+
references.sort((a, b) => a.sort - b.sort),
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return referencesByTargetUuid;
|
|
229
|
+
};
|