@eightyfourthousand/data-access 2026.3.1 → 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/package.json +1 -1
- 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,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
|
+
};
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { DataClient, PassageRowDTO } from '../types';
|
|
2
|
+
|
|
3
|
+
type ApiPaginationDirection = 'FORWARD' | 'BACKWARD' | 'AROUND';
|
|
4
|
+
|
|
5
|
+
export type PassageConnectionNode = {
|
|
6
|
+
uuid: string;
|
|
7
|
+
workUuid: string;
|
|
8
|
+
content: string;
|
|
9
|
+
label: string | null;
|
|
10
|
+
sort: number;
|
|
11
|
+
type: string;
|
|
12
|
+
toh: string | null;
|
|
13
|
+
xmlId: string | null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type PassageConnectionPage = {
|
|
17
|
+
nodes: PassageConnectionNode[];
|
|
18
|
+
nextCursor: string | null;
|
|
19
|
+
prevCursor: string | null;
|
|
20
|
+
hasMoreAfter: boolean;
|
|
21
|
+
hasMoreBefore: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const EMPTY_PASSAGE_CONNECTION: PassageConnectionPage = {
|
|
25
|
+
nodes: [],
|
|
26
|
+
nextCursor: null,
|
|
27
|
+
prevCursor: null,
|
|
28
|
+
hasMoreAfter: false,
|
|
29
|
+
hasMoreBefore: false,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const DEFAULT_PASSAGE_CONNECTION_LIMIT = 20;
|
|
33
|
+
const MAX_PASSAGE_CONNECTION_LIMIT = 100;
|
|
34
|
+
|
|
35
|
+
function buildPassageConnection(
|
|
36
|
+
nodes: PassageConnectionNode[],
|
|
37
|
+
nextCursor: string | null,
|
|
38
|
+
prevCursor: string | null,
|
|
39
|
+
hasMoreAfter: boolean,
|
|
40
|
+
hasMoreBefore: boolean,
|
|
41
|
+
): PassageConnectionPage {
|
|
42
|
+
return {
|
|
43
|
+
nodes,
|
|
44
|
+
nextCursor,
|
|
45
|
+
prevCursor,
|
|
46
|
+
hasMoreAfter,
|
|
47
|
+
hasMoreBefore,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function rowToPassageConnectionNode(
|
|
52
|
+
row: PassageRowDTO,
|
|
53
|
+
workUuid: string,
|
|
54
|
+
): PassageConnectionNode {
|
|
55
|
+
return {
|
|
56
|
+
uuid: row.uuid,
|
|
57
|
+
workUuid,
|
|
58
|
+
content: row.content,
|
|
59
|
+
label: row.label,
|
|
60
|
+
sort: row.sort,
|
|
61
|
+
type: row.type,
|
|
62
|
+
toh: row.toh ?? null,
|
|
63
|
+
xmlId: row.xmlId ?? null,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const getWorkPassagesConnection = async ({
|
|
68
|
+
client,
|
|
69
|
+
workUuid,
|
|
70
|
+
cursor,
|
|
71
|
+
limit = DEFAULT_PASSAGE_CONNECTION_LIMIT,
|
|
72
|
+
filter,
|
|
73
|
+
direction = 'FORWARD',
|
|
74
|
+
}: {
|
|
75
|
+
client: DataClient;
|
|
76
|
+
workUuid: string;
|
|
77
|
+
cursor?: string;
|
|
78
|
+
limit?: number;
|
|
79
|
+
filter?: { type?: string; types?: string[]; label?: string };
|
|
80
|
+
direction?: ApiPaginationDirection;
|
|
81
|
+
}): Promise<PassageConnectionPage> => {
|
|
82
|
+
const clampedLimit = Math.min(
|
|
83
|
+
Math.max(limit, 1),
|
|
84
|
+
MAX_PASSAGE_CONNECTION_LIMIT,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (direction === 'AROUND') {
|
|
88
|
+
return getWorkPassagesAround({
|
|
89
|
+
client,
|
|
90
|
+
workUuid,
|
|
91
|
+
cursor,
|
|
92
|
+
limit: clampedLimit,
|
|
93
|
+
filter,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const isForward = direction === 'FORWARD';
|
|
98
|
+
let cursorSort: number | null = null;
|
|
99
|
+
if (cursor) {
|
|
100
|
+
const { data: cursorPassage } = await client
|
|
101
|
+
.from('passages')
|
|
102
|
+
.select('sort')
|
|
103
|
+
.eq('uuid', cursor)
|
|
104
|
+
.single();
|
|
105
|
+
|
|
106
|
+
if (cursorPassage) {
|
|
107
|
+
cursorSort = cursorPassage.sort;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let query = client
|
|
112
|
+
.from('passages')
|
|
113
|
+
.select('uuid, content, label, sort, type, toh, xmlId, work_uuid')
|
|
114
|
+
.eq('work_uuid', workUuid)
|
|
115
|
+
.order('sort', { ascending: isForward })
|
|
116
|
+
.limit(clampedLimit + 1);
|
|
117
|
+
|
|
118
|
+
if (cursorSort !== null) {
|
|
119
|
+
query = isForward
|
|
120
|
+
? query.gt('sort', cursorSort)
|
|
121
|
+
: query.lt('sort', cursorSort);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (filter?.types && filter.types.length > 0) {
|
|
125
|
+
query = query.in('type', filter.types);
|
|
126
|
+
} else if (filter?.type) {
|
|
127
|
+
query = query.filter('type', 'match', `${filter.type}.*`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (filter?.label) {
|
|
131
|
+
query = query.ilike('label', filter.label);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const { data, error } = await query;
|
|
135
|
+
|
|
136
|
+
if (error) {
|
|
137
|
+
console.error('Error fetching passages:', error);
|
|
138
|
+
return EMPTY_PASSAGE_CONNECTION;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const passages = (data ?? []) as PassageRowDTO[];
|
|
142
|
+
const hasMore = passages.length > clampedLimit;
|
|
143
|
+
let resultPassages = hasMore ? passages.slice(0, clampedLimit) : passages;
|
|
144
|
+
|
|
145
|
+
if (!isForward) {
|
|
146
|
+
resultPassages = resultPassages.reverse();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const hasMoreAfter = isForward ? hasMore : cursorSort !== null;
|
|
150
|
+
const hasMoreBefore = isForward ? cursorSort !== null : hasMore;
|
|
151
|
+
|
|
152
|
+
if (resultPassages.length === 0) {
|
|
153
|
+
return buildPassageConnection([], null, null, false, hasMoreBefore);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const nodes = resultPassages.map((row) =>
|
|
157
|
+
rowToPassageConnectionNode(row, workUuid),
|
|
158
|
+
);
|
|
159
|
+
const firstPassage = resultPassages[0];
|
|
160
|
+
const lastPassage = resultPassages[resultPassages.length - 1];
|
|
161
|
+
|
|
162
|
+
return buildPassageConnection(
|
|
163
|
+
nodes,
|
|
164
|
+
hasMoreAfter ? lastPassage.uuid : null,
|
|
165
|
+
hasMoreBefore ? firstPassage.uuid : null,
|
|
166
|
+
hasMoreAfter,
|
|
167
|
+
hasMoreBefore,
|
|
168
|
+
);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export const getWorkPassagesAround = async ({
|
|
172
|
+
client,
|
|
173
|
+
workUuid,
|
|
174
|
+
cursor,
|
|
175
|
+
limit,
|
|
176
|
+
filter,
|
|
177
|
+
}: {
|
|
178
|
+
client: DataClient;
|
|
179
|
+
workUuid: string;
|
|
180
|
+
cursor?: string;
|
|
181
|
+
limit: number;
|
|
182
|
+
filter?: { type?: string; types?: string[]; label?: string };
|
|
183
|
+
}): Promise<PassageConnectionPage> => {
|
|
184
|
+
if (!cursor) {
|
|
185
|
+
console.error('AROUND direction requires a cursor');
|
|
186
|
+
return EMPTY_PASSAGE_CONNECTION;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const { data: cursorPassage } = await client
|
|
190
|
+
.from('passages')
|
|
191
|
+
.select('sort')
|
|
192
|
+
.eq('uuid', cursor)
|
|
193
|
+
.single();
|
|
194
|
+
|
|
195
|
+
if (!cursorPassage) {
|
|
196
|
+
console.error('Cursor passage not found');
|
|
197
|
+
return EMPTY_PASSAGE_CONNECTION;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const cursorSort = cursorPassage.sort;
|
|
201
|
+
const limitBefore = Math.floor(limit / 2);
|
|
202
|
+
const limitAfter = limit - limitBefore;
|
|
203
|
+
const baseSelect = 'uuid, content, label, sort, type, toh, xmlId, work_uuid';
|
|
204
|
+
|
|
205
|
+
let beforeQuery = client
|
|
206
|
+
.from('passages')
|
|
207
|
+
.select(baseSelect)
|
|
208
|
+
.eq('work_uuid', workUuid)
|
|
209
|
+
.lt('sort', cursorSort)
|
|
210
|
+
.order('sort', { ascending: false })
|
|
211
|
+
.limit(limitBefore + 1);
|
|
212
|
+
|
|
213
|
+
let afterQuery = client
|
|
214
|
+
.from('passages')
|
|
215
|
+
.select(baseSelect)
|
|
216
|
+
.eq('work_uuid', workUuid)
|
|
217
|
+
.gte('sort', cursorSort)
|
|
218
|
+
.order('sort', { ascending: true })
|
|
219
|
+
.limit(limitAfter + 1);
|
|
220
|
+
|
|
221
|
+
if (filter?.types && filter.types.length > 0) {
|
|
222
|
+
beforeQuery = beforeQuery.in('type', filter.types);
|
|
223
|
+
afterQuery = afterQuery.in('type', filter.types);
|
|
224
|
+
} else if (filter?.type) {
|
|
225
|
+
const pattern = `${filter.type}.*`;
|
|
226
|
+
beforeQuery = beforeQuery.filter('type', 'match', pattern);
|
|
227
|
+
afterQuery = afterQuery.filter('type', 'match', pattern);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (filter?.label) {
|
|
231
|
+
beforeQuery = beforeQuery.ilike('label', filter.label);
|
|
232
|
+
afterQuery = afterQuery.ilike('label', filter.label);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const [beforeResult, afterResult] = await Promise.all([
|
|
236
|
+
beforeQuery,
|
|
237
|
+
afterQuery,
|
|
238
|
+
]);
|
|
239
|
+
|
|
240
|
+
if (beforeResult.error || afterResult.error) {
|
|
241
|
+
console.error(
|
|
242
|
+
'Error fetching passages around:',
|
|
243
|
+
beforeResult.error || afterResult.error,
|
|
244
|
+
);
|
|
245
|
+
return EMPTY_PASSAGE_CONNECTION;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const passagesBefore = (beforeResult.data ?? []) as PassageRowDTO[];
|
|
249
|
+
const passagesAfter = (afterResult.data ?? []) as PassageRowDTO[];
|
|
250
|
+
const hasMoreBefore = passagesBefore.length > limitBefore;
|
|
251
|
+
const hasMoreAfter = passagesAfter.length > limitAfter;
|
|
252
|
+
const trimmedBefore = hasMoreBefore
|
|
253
|
+
? passagesBefore.slice(0, limitBefore)
|
|
254
|
+
: passagesBefore;
|
|
255
|
+
const trimmedAfter = hasMoreAfter
|
|
256
|
+
? passagesAfter.slice(0, limitAfter)
|
|
257
|
+
: passagesAfter;
|
|
258
|
+
const resultPassages = [...trimmedBefore.reverse(), ...trimmedAfter];
|
|
259
|
+
|
|
260
|
+
if (resultPassages.length === 0) {
|
|
261
|
+
return EMPTY_PASSAGE_CONNECTION;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const nodes = resultPassages.map((row) =>
|
|
265
|
+
rowToPassageConnectionNode(row, workUuid),
|
|
266
|
+
);
|
|
267
|
+
const firstPassage = resultPassages[0];
|
|
268
|
+
const lastPassage = resultPassages[resultPassages.length - 1];
|
|
269
|
+
|
|
270
|
+
return buildPassageConnection(
|
|
271
|
+
nodes,
|
|
272
|
+
hasMoreAfter ? lastPassage.uuid : null,
|
|
273
|
+
hasMoreBefore ? firstPassage.uuid : null,
|
|
274
|
+
hasMoreAfter,
|
|
275
|
+
hasMoreBefore,
|
|
276
|
+
);
|
|
277
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DataClient,
|
|
3
|
+
PassageDTO,
|
|
4
|
+
annotationsFromDTO,
|
|
5
|
+
passageFromDTO,
|
|
6
|
+
} from '../types';
|
|
7
|
+
|
|
8
|
+
import { PassageConnectionNode } from './pagination';
|
|
9
|
+
|
|
10
|
+
export const getPassage = async ({
|
|
11
|
+
client,
|
|
12
|
+
uuid,
|
|
13
|
+
}: {
|
|
14
|
+
client: DataClient;
|
|
15
|
+
uuid: string;
|
|
16
|
+
}) => {
|
|
17
|
+
const { data } = await client
|
|
18
|
+
.rpc('get_passage_with_annotations', {
|
|
19
|
+
uuid_input: uuid,
|
|
20
|
+
})
|
|
21
|
+
.single();
|
|
22
|
+
|
|
23
|
+
if (!data) {
|
|
24
|
+
console.warn(`No passage found for uuid: ${uuid}`);
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const dto = data as PassageDTO;
|
|
29
|
+
return passageFromDTO(
|
|
30
|
+
dto,
|
|
31
|
+
annotationsFromDTO(dto?.annotations || [], dto?.content.length || 0),
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const getPassageByUuidOrXmlId = async ({
|
|
36
|
+
client,
|
|
37
|
+
uuid,
|
|
38
|
+
xmlId,
|
|
39
|
+
}: {
|
|
40
|
+
client: DataClient;
|
|
41
|
+
uuid?: string;
|
|
42
|
+
xmlId?: string;
|
|
43
|
+
}): Promise<PassageConnectionNode | null> => {
|
|
44
|
+
if (!uuid && !xmlId) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let query = client
|
|
49
|
+
.from('passages')
|
|
50
|
+
.select('uuid, content, label, sort, type, xmlId, toh, work_uuid');
|
|
51
|
+
|
|
52
|
+
if (uuid) {
|
|
53
|
+
query = query.eq('uuid', uuid);
|
|
54
|
+
} else if (xmlId) {
|
|
55
|
+
query = query.eq('xmlId', xmlId);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const { data, error } = await query.single();
|
|
59
|
+
|
|
60
|
+
if (error) {
|
|
61
|
+
console.error(`Error fetching passage ${uuid || xmlId}:`, error);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!data) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
uuid: data.uuid,
|
|
71
|
+
workUuid: data.work_uuid,
|
|
72
|
+
content: data.content,
|
|
73
|
+
label: data.label,
|
|
74
|
+
sort: data.sort,
|
|
75
|
+
type: data.type,
|
|
76
|
+
toh: data.toh ?? null,
|
|
77
|
+
xmlId: data.xmlId ?? null,
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const getPassageUuidByXmlId = async ({
|
|
82
|
+
client,
|
|
83
|
+
xmlId,
|
|
84
|
+
}: {
|
|
85
|
+
client: DataClient;
|
|
86
|
+
xmlId: string;
|
|
87
|
+
}) => {
|
|
88
|
+
const { data, error } = await client
|
|
89
|
+
.from('passages')
|
|
90
|
+
.select('uuid, workUuid:work_uuid')
|
|
91
|
+
.eq('xmlId', xmlId)
|
|
92
|
+
.single();
|
|
93
|
+
|
|
94
|
+
if (error) {
|
|
95
|
+
console.error(`Error fetching passage uuid for xmlId: ${xmlId}`, error);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return data?.uuid;
|
|
100
|
+
};
|