@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,153 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ANNOTATIONS_TO_IGNORE,
|
|
3
|
+
AnnotationDTO,
|
|
4
|
+
DataClient,
|
|
5
|
+
Passage,
|
|
6
|
+
passagesToDTO,
|
|
7
|
+
passagesToRowDTO,
|
|
8
|
+
} from '../types';
|
|
9
|
+
|
|
10
|
+
export type ReplacePassageRow = {
|
|
11
|
+
content: string;
|
|
12
|
+
label: string | null;
|
|
13
|
+
sort: number;
|
|
14
|
+
toh: string | null;
|
|
15
|
+
type: string;
|
|
16
|
+
uuid: string;
|
|
17
|
+
work_uuid: string;
|
|
18
|
+
xmlId: string | null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const fetchReplaceRows = async ({
|
|
22
|
+
client,
|
|
23
|
+
targetUuids,
|
|
24
|
+
}: {
|
|
25
|
+
client: DataClient;
|
|
26
|
+
targetUuids: string[];
|
|
27
|
+
}) => {
|
|
28
|
+
const { data, error } = await client
|
|
29
|
+
.from('passages')
|
|
30
|
+
.select('uuid, work_uuid, content, label, sort, type, toh, xmlId')
|
|
31
|
+
.in('uuid', targetUuids);
|
|
32
|
+
|
|
33
|
+
if (error) {
|
|
34
|
+
console.error('Error fetching replace targets:', error);
|
|
35
|
+
return {
|
|
36
|
+
ok: false as const,
|
|
37
|
+
error: `Failed to fetch replace targets: ${error.message}`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const rowsByUuid = new Map((data ?? []).map((row) => [row.uuid, row]));
|
|
42
|
+
const missingUuids = targetUuids.filter((uuid) => !rowsByUuid.has(uuid));
|
|
43
|
+
if (missingUuids.length > 0) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false as const,
|
|
46
|
+
error: `Unknown target UUIDs: ${missingUuids.join(', ')}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
ok: true as const,
|
|
52
|
+
orderedRows: targetUuids
|
|
53
|
+
.map((uuid) => rowsByUuid.get(uuid))
|
|
54
|
+
.filter((row): row is ReplacePassageRow => Boolean(row)),
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const fetchReplaceAnnotations = async ({
|
|
59
|
+
client,
|
|
60
|
+
targetUuids,
|
|
61
|
+
}: {
|
|
62
|
+
client: DataClient;
|
|
63
|
+
targetUuids: string[];
|
|
64
|
+
}) => {
|
|
65
|
+
const { data, error } = await client
|
|
66
|
+
.from('passage_annotations')
|
|
67
|
+
.select('uuid, content, end, start, type, passage_uuid, toh')
|
|
68
|
+
.in('passage_uuid', targetUuids)
|
|
69
|
+
.not('type', 'in', `(${ANNOTATIONS_TO_IGNORE.join(',')})`);
|
|
70
|
+
|
|
71
|
+
if (error) {
|
|
72
|
+
console.error('Error fetching annotations for replace:', error);
|
|
73
|
+
return {
|
|
74
|
+
ok: false as const,
|
|
75
|
+
error: `Failed to fetch annotations: ${error.message}`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
ok: true as const,
|
|
81
|
+
rawAnnotations: (data ?? []) as AnnotationDTO[],
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const persistReplaceChanges = async ({
|
|
86
|
+
client,
|
|
87
|
+
rawAnnotations,
|
|
88
|
+
updatedPassages,
|
|
89
|
+
}: {
|
|
90
|
+
client: DataClient;
|
|
91
|
+
rawAnnotations: AnnotationDTO[];
|
|
92
|
+
updatedPassages: Passage[];
|
|
93
|
+
}) => {
|
|
94
|
+
const passageRowDtos = passagesToRowDTO(updatedPassages);
|
|
95
|
+
const annotationDtos = passagesToDTO(updatedPassages).flatMap(
|
|
96
|
+
(passage) => passage.annotations || [],
|
|
97
|
+
);
|
|
98
|
+
const updatedPassageUuids = updatedPassages.map((passage) => passage.uuid);
|
|
99
|
+
const updatedAnnotationUuids = new Set(annotationDtos.map((a) => a.uuid));
|
|
100
|
+
const annotationsToDelete = rawAnnotations.filter(
|
|
101
|
+
(annotation) =>
|
|
102
|
+
updatedPassageUuids.includes(annotation.passage_uuid || '') &&
|
|
103
|
+
!updatedAnnotationUuids.has(annotation.uuid),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const { error: passageError } = await client
|
|
107
|
+
.from('passages')
|
|
108
|
+
.upsert(passageRowDtos, { onConflict: 'uuid' });
|
|
109
|
+
|
|
110
|
+
if (passageError) {
|
|
111
|
+
console.error('Error saving replaced passages:', passageError);
|
|
112
|
+
return {
|
|
113
|
+
ok: false as const,
|
|
114
|
+
error: `Failed to save replaced passages: ${passageError.message}`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (annotationDtos.length > 0) {
|
|
119
|
+
const { error: annotationError } = await client
|
|
120
|
+
.from('passage_annotations')
|
|
121
|
+
.upsert(annotationDtos, { onConflict: 'uuid' });
|
|
122
|
+
|
|
123
|
+
if (annotationError) {
|
|
124
|
+
console.error('Error saving replaced annotations:', annotationError);
|
|
125
|
+
return {
|
|
126
|
+
ok: false as const,
|
|
127
|
+
error: `Failed to save replaced annotations: ${annotationError.message}`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (annotationsToDelete.length > 0) {
|
|
133
|
+
const { error: deleteError } = await client
|
|
134
|
+
.from('passage_annotations')
|
|
135
|
+
.delete()
|
|
136
|
+
.in(
|
|
137
|
+
'uuid',
|
|
138
|
+
annotationsToDelete.map((annotation) => annotation.uuid),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (deleteError) {
|
|
142
|
+
console.error('Error deleting replaced annotations:', deleteError);
|
|
143
|
+
return {
|
|
144
|
+
ok: false as const,
|
|
145
|
+
error: `Failed to delete replaced annotations: ${deleteError.message}`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
ok: true as const,
|
|
152
|
+
};
|
|
153
|
+
};
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ANNOTATIONS_TO_IGNORE,
|
|
3
|
+
DataClient,
|
|
4
|
+
Passage,
|
|
5
|
+
passagesToDTO,
|
|
6
|
+
passagesToRowDTO,
|
|
7
|
+
} from '../types';
|
|
8
|
+
|
|
9
|
+
const SAVE_PAGE_SIZE = 500;
|
|
10
|
+
|
|
11
|
+
export const savePassages = async ({
|
|
12
|
+
client,
|
|
13
|
+
passages,
|
|
14
|
+
}: {
|
|
15
|
+
client: DataClient;
|
|
16
|
+
passages: Passage[];
|
|
17
|
+
}) => {
|
|
18
|
+
/**
|
|
19
|
+
* 1. Extract all annotations from savePassages
|
|
20
|
+
* 2. Query the database for all current annotations for the savePassages
|
|
21
|
+
* 3. Determine which annotations need to be deleted or upserted
|
|
22
|
+
* 4. Upsert the passages
|
|
23
|
+
* 5. Upsert the annotations
|
|
24
|
+
* 6. Delete the annotations that are no longer present
|
|
25
|
+
*/
|
|
26
|
+
const dtos = passagesToDTO(passages);
|
|
27
|
+
const passageRowDtos = passagesToRowDTO(passages);
|
|
28
|
+
const passageUuids = passages.map((p) => p.uuid);
|
|
29
|
+
const annotations = dtos.flatMap((p) => p.annotations || []);
|
|
30
|
+
|
|
31
|
+
const { data: existingAnnotations } = await client
|
|
32
|
+
.from('passage_annotations')
|
|
33
|
+
.select(`uuid`)
|
|
34
|
+
.in('passage_uuid', passageUuids)
|
|
35
|
+
.not('type', 'in', `(${ANNOTATIONS_TO_IGNORE.join(',')})`);
|
|
36
|
+
|
|
37
|
+
const annotationsToDelete = existingAnnotations?.filter(
|
|
38
|
+
(ea) => !annotations.find((a) => a.uuid === ea.uuid),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const { error: passageError } = await client
|
|
42
|
+
.from('passages')
|
|
43
|
+
.upsert(passageRowDtos, { onConflict: 'uuid' });
|
|
44
|
+
|
|
45
|
+
if (passageError) {
|
|
46
|
+
console.error('Error saving passages:', passageError);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (annotations.length > 0) {
|
|
51
|
+
const { error: annotationError } = await client
|
|
52
|
+
.from('passage_annotations')
|
|
53
|
+
.upsert(annotations, { onConflict: 'uuid' });
|
|
54
|
+
|
|
55
|
+
if (annotationError) {
|
|
56
|
+
console.error('Error saving annotations:', annotationError);
|
|
57
|
+
throw annotationError;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (annotationsToDelete && annotationsToDelete.length > 0) {
|
|
62
|
+
const { error: deleteError } = await client
|
|
63
|
+
.from('passage_annotations')
|
|
64
|
+
.delete()
|
|
65
|
+
.in(
|
|
66
|
+
'uuid',
|
|
67
|
+
annotationsToDelete.map((a) => a.uuid),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (deleteError) {
|
|
71
|
+
console.error('Error deleting annotations:', deleteError);
|
|
72
|
+
throw deleteError;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
async function normalizePassageLabelsAfter({
|
|
78
|
+
client,
|
|
79
|
+
workUuid,
|
|
80
|
+
fromSort,
|
|
81
|
+
fromLabel,
|
|
82
|
+
delta,
|
|
83
|
+
}: {
|
|
84
|
+
client: DataClient;
|
|
85
|
+
workUuid: string;
|
|
86
|
+
fromSort: number;
|
|
87
|
+
fromLabel: string;
|
|
88
|
+
delta: number;
|
|
89
|
+
}): Promise<void> {
|
|
90
|
+
const parts = fromLabel.split('.');
|
|
91
|
+
const depth = parts.length;
|
|
92
|
+
const prefix = depth > 1 ? `${parts.slice(0, -1).join('.')}.` : '';
|
|
93
|
+
let nextInt = Number.parseInt(parts[depth - 1], 10) + Math.max(delta, 0);
|
|
94
|
+
let lastSort = fromSort;
|
|
95
|
+
let done = false;
|
|
96
|
+
|
|
97
|
+
while (!done) {
|
|
98
|
+
const { data, error } = await client
|
|
99
|
+
.from('passages')
|
|
100
|
+
.select('uuid, label, sort')
|
|
101
|
+
.eq('work_uuid', workUuid)
|
|
102
|
+
.gt('sort', lastSort)
|
|
103
|
+
.order('sort', { ascending: true })
|
|
104
|
+
.limit(SAVE_PAGE_SIZE);
|
|
105
|
+
|
|
106
|
+
if (error || !data || data.length === 0) break;
|
|
107
|
+
|
|
108
|
+
const labelUpdates: { uuid: string; label: string }[] = [];
|
|
109
|
+
const prefixRenames: { oldPrefix: string; newPrefix: string }[] = [];
|
|
110
|
+
|
|
111
|
+
for (const row of data) {
|
|
112
|
+
const rowParts = (row.label ?? '').split('.');
|
|
113
|
+
|
|
114
|
+
if (rowParts.length < depth || !row.label?.startsWith(prefix)) {
|
|
115
|
+
done = true;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (rowParts.length > depth) continue;
|
|
120
|
+
|
|
121
|
+
const expectedLabel = prefix + nextInt;
|
|
122
|
+
if (row.label === expectedLabel) {
|
|
123
|
+
done = true;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
labelUpdates.push({ uuid: row.uuid, label: expectedLabel });
|
|
128
|
+
prefixRenames.push({
|
|
129
|
+
oldPrefix: `${row.label}.`,
|
|
130
|
+
newPrefix: `${expectedLabel}.`,
|
|
131
|
+
});
|
|
132
|
+
nextInt++;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (labelUpdates.length > 0) {
|
|
136
|
+
const { error: upsertError } = await client
|
|
137
|
+
.from('passages')
|
|
138
|
+
.upsert(labelUpdates, { onConflict: 'uuid' });
|
|
139
|
+
if (upsertError) {
|
|
140
|
+
console.error('Error normalizing passage labels:', upsertError);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const { oldPrefix, newPrefix } of prefixRenames) {
|
|
144
|
+
const { error: prefixError } = await client.rpc(
|
|
145
|
+
'rename_passage_label_prefix',
|
|
146
|
+
{
|
|
147
|
+
p_work_uuid: workUuid,
|
|
148
|
+
p_old_prefix: oldPrefix,
|
|
149
|
+
p_new_prefix: newPrefix,
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
if (prefixError) {
|
|
153
|
+
console.error('Error renaming passage label prefix:', prefixError);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (done || data.length < SAVE_PAGE_SIZE) break;
|
|
159
|
+
lastSort = data[data.length - 1].sort;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export type SavePassagesWithDeletionsResult = {
|
|
164
|
+
success: boolean;
|
|
165
|
+
savedCount: number;
|
|
166
|
+
deletedCount?: number;
|
|
167
|
+
error?: string;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export const savePassagesWithDeletions = async ({
|
|
171
|
+
client,
|
|
172
|
+
passages,
|
|
173
|
+
deletedUuids = [],
|
|
174
|
+
}: {
|
|
175
|
+
client: DataClient;
|
|
176
|
+
passages: Passage[];
|
|
177
|
+
deletedUuids?: string[];
|
|
178
|
+
}): Promise<SavePassagesWithDeletionsResult> => {
|
|
179
|
+
const inputUuids = passages.map((p) => p.uuid);
|
|
180
|
+
const { data: existingRows } = await client
|
|
181
|
+
.from('passages')
|
|
182
|
+
.select('uuid')
|
|
183
|
+
.in('uuid', inputUuids);
|
|
184
|
+
const existingUuidSet = new Set((existingRows ?? []).map((r) => r.uuid));
|
|
185
|
+
const newPassages = passages.filter((p) => !existingUuidSet.has(p.uuid));
|
|
186
|
+
const sortedNewPassages = [...newPassages].sort((a, b) => b.sort - a.sort);
|
|
187
|
+
|
|
188
|
+
for (const passage of sortedNewPassages) {
|
|
189
|
+
const { error } = await client.rpc('shift_passage_sorts', {
|
|
190
|
+
p_work_uuid: passage.workUuid,
|
|
191
|
+
p_from_sort: passage.sort,
|
|
192
|
+
p_delta: 1,
|
|
193
|
+
});
|
|
194
|
+
if (error) {
|
|
195
|
+
console.error('Error shifting passage sorts:', error);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
for (const passage of sortedNewPassages) {
|
|
200
|
+
await normalizePassageLabelsAfter({
|
|
201
|
+
client,
|
|
202
|
+
workUuid: passage.workUuid,
|
|
203
|
+
fromSort: passage.sort,
|
|
204
|
+
fromLabel: passage.label,
|
|
205
|
+
delta: 1,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let deletedCount = 0;
|
|
210
|
+
if (deletedUuids.length > 0) {
|
|
211
|
+
const { data: deletedPassages } = await client
|
|
212
|
+
.from('passages')
|
|
213
|
+
.select('uuid, sort, label, work_uuid')
|
|
214
|
+
.in('uuid', deletedUuids);
|
|
215
|
+
|
|
216
|
+
if (deletedPassages && deletedPassages.length > 0) {
|
|
217
|
+
const sortedDeleted = [...deletedPassages].sort(
|
|
218
|
+
(a, b) => b.sort - a.sort,
|
|
219
|
+
);
|
|
220
|
+
for (const deletedPassage of sortedDeleted) {
|
|
221
|
+
await normalizePassageLabelsAfter({
|
|
222
|
+
client,
|
|
223
|
+
workUuid: deletedPassage.work_uuid,
|
|
224
|
+
fromSort: deletedPassage.sort,
|
|
225
|
+
fromLabel: deletedPassage.label,
|
|
226
|
+
delta: -1,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const { error: deleteAnnotationsError } = await client
|
|
231
|
+
.from('passage_annotations')
|
|
232
|
+
.delete()
|
|
233
|
+
.in('passage_uuid', deletedUuids);
|
|
234
|
+
|
|
235
|
+
if (deleteAnnotationsError) {
|
|
236
|
+
console.error(
|
|
237
|
+
'Error deleting annotations for deleted passages:',
|
|
238
|
+
deleteAnnotationsError,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const { error: deletePassagesError } = await client
|
|
243
|
+
.from('passages')
|
|
244
|
+
.delete()
|
|
245
|
+
.in('uuid', deletedUuids);
|
|
246
|
+
|
|
247
|
+
if (deletePassagesError) {
|
|
248
|
+
console.error('Error deleting passages:', deletePassagesError);
|
|
249
|
+
return {
|
|
250
|
+
success: false,
|
|
251
|
+
savedCount: 0,
|
|
252
|
+
error: `Failed to delete passages: ${deletePassagesError.message}`,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
deletedCount = deletedPassages.length;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const dtos = passagesToDTO(passages);
|
|
261
|
+
const passageRowDtos = passagesToRowDTO(passages);
|
|
262
|
+
const passageUuids = passages.map((p) => p.uuid);
|
|
263
|
+
const annotations = dtos.flatMap((p) => p.annotations || []);
|
|
264
|
+
const { data: existingAnnotations } = await client
|
|
265
|
+
.from('passage_annotations')
|
|
266
|
+
.select('uuid')
|
|
267
|
+
.in('passage_uuid', passageUuids)
|
|
268
|
+
.not('type', 'in', `(${ANNOTATIONS_TO_IGNORE.join(',')})`);
|
|
269
|
+
|
|
270
|
+
const annotationsToDelete = existingAnnotations?.filter(
|
|
271
|
+
(existingAnnotation) =>
|
|
272
|
+
!annotations.find(
|
|
273
|
+
(annotation) => annotation.uuid === existingAnnotation.uuid,
|
|
274
|
+
),
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const { error: passageError } = await client
|
|
278
|
+
.from('passages')
|
|
279
|
+
.upsert(passageRowDtos, { onConflict: 'uuid' });
|
|
280
|
+
|
|
281
|
+
if (passageError) {
|
|
282
|
+
console.error('Error saving passages:', passageError);
|
|
283
|
+
return {
|
|
284
|
+
success: false,
|
|
285
|
+
savedCount: 0,
|
|
286
|
+
error: `Failed to save passages: ${passageError.message}`,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (annotations.length > 0) {
|
|
291
|
+
const { error: annotationError } = await client
|
|
292
|
+
.from('passage_annotations')
|
|
293
|
+
.upsert(annotations, { onConflict: 'uuid' });
|
|
294
|
+
|
|
295
|
+
if (annotationError) {
|
|
296
|
+
console.error('Error saving annotations:', annotationError);
|
|
297
|
+
return {
|
|
298
|
+
success: false,
|
|
299
|
+
savedCount: passages.length,
|
|
300
|
+
error: `Passages saved but annotations failed: ${annotationError.message}`,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (annotationsToDelete && annotationsToDelete.length > 0) {
|
|
306
|
+
const { error: deleteError } = await client
|
|
307
|
+
.from('passage_annotations')
|
|
308
|
+
.delete()
|
|
309
|
+
.in(
|
|
310
|
+
'uuid',
|
|
311
|
+
annotationsToDelete.map((annotation) => annotation.uuid),
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
if (deleteError) {
|
|
315
|
+
console.error('Error deleting annotations:', deleteError);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
success: true,
|
|
321
|
+
savedCount: passages.length,
|
|
322
|
+
deletedCount,
|
|
323
|
+
};
|
|
324
|
+
};
|
package/src/lib/publications.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BodyItemType,
|
|
3
3
|
DataClient,
|
|
4
|
+
Title,
|
|
4
5
|
TitlesDTO,
|
|
5
6
|
WorkDTO,
|
|
6
7
|
titlesFromDTO,
|
|
@@ -14,6 +15,19 @@ import {
|
|
|
14
15
|
Work,
|
|
15
16
|
} from './types';
|
|
16
17
|
|
|
18
|
+
type WorksPageInfo = {
|
|
19
|
+
nextCursor: string | null;
|
|
20
|
+
prevCursor: string | null;
|
|
21
|
+
hasMoreAfter: boolean;
|
|
22
|
+
hasMoreBefore: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type WorksPage = {
|
|
26
|
+
items: Work[];
|
|
27
|
+
pageInfo: WorksPageInfo;
|
|
28
|
+
totalCount: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
17
31
|
export const getTranslationUuids = async ({
|
|
18
32
|
client,
|
|
19
33
|
}: {
|
|
@@ -23,6 +37,29 @@ export const getTranslationUuids = async ({
|
|
|
23
37
|
return data?.map(({ uuid }: { uuid: string }) => uuid) || [];
|
|
24
38
|
};
|
|
25
39
|
|
|
40
|
+
export const getWorkUuidByToh = async ({
|
|
41
|
+
client,
|
|
42
|
+
toh,
|
|
43
|
+
}: {
|
|
44
|
+
client: DataClient;
|
|
45
|
+
toh: string;
|
|
46
|
+
}): Promise<string | null> => {
|
|
47
|
+
const { data, error } = await client
|
|
48
|
+
.from('work_toh')
|
|
49
|
+
.select('work_uuid')
|
|
50
|
+
.eq('toh_clean', toh)
|
|
51
|
+
.single();
|
|
52
|
+
|
|
53
|
+
if (error || !data) {
|
|
54
|
+
if (error) {
|
|
55
|
+
console.error('Error fetching work UUID by TOH:', error);
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return data.work_uuid ?? null;
|
|
61
|
+
};
|
|
62
|
+
|
|
26
63
|
export const getTranslationPassages = async ({
|
|
27
64
|
client,
|
|
28
65
|
uuid,
|
|
@@ -110,6 +147,54 @@ export const getTranslationTitles = async ({
|
|
|
110
147
|
return titlesFromDTO(data as TitlesDTO);
|
|
111
148
|
};
|
|
112
149
|
|
|
150
|
+
export const getWorkTitles = async ({
|
|
151
|
+
client,
|
|
152
|
+
uuid,
|
|
153
|
+
}: {
|
|
154
|
+
client: DataClient;
|
|
155
|
+
uuid: string;
|
|
156
|
+
}): Promise<Title[]> => {
|
|
157
|
+
const { data, error } = await client.rpc('get_work_titles', {
|
|
158
|
+
work_uuid_input: uuid,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (error) {
|
|
162
|
+
console.error('Error fetching work titles:', error);
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return titlesFromDTO(data as TitlesDTO);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export const getWorkTitlesByUuids = async ({
|
|
170
|
+
client,
|
|
171
|
+
uuids,
|
|
172
|
+
}: {
|
|
173
|
+
client: DataClient;
|
|
174
|
+
uuids: readonly string[];
|
|
175
|
+
}): Promise<Map<string, string>> => {
|
|
176
|
+
const titlesByUuid = new Map<string, string>();
|
|
177
|
+
if (uuids.length === 0) return titlesByUuid;
|
|
178
|
+
|
|
179
|
+
const { data, error } = await client
|
|
180
|
+
.from('works')
|
|
181
|
+
.select('uuid, title')
|
|
182
|
+
.in('uuid', uuids as string[]);
|
|
183
|
+
|
|
184
|
+
if (error) {
|
|
185
|
+
console.error('Error batch loading work titles:', error);
|
|
186
|
+
return titlesByUuid;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for (const work of data ?? []) {
|
|
190
|
+
if (work.title) {
|
|
191
|
+
titlesByUuid.set(work.uuid, work.title);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return titlesByUuid;
|
|
196
|
+
};
|
|
197
|
+
|
|
113
198
|
export const getTranslationMetadataByUuid = async ({
|
|
114
199
|
client,
|
|
115
200
|
uuid,
|
|
@@ -211,3 +296,86 @@ export const getTranslationsMetadata = async ({
|
|
|
211
296
|
const dto = data as WorkDTO[];
|
|
212
297
|
return dto?.map((work) => workFromDTO(work as WorkDTO)) || [];
|
|
213
298
|
};
|
|
299
|
+
|
|
300
|
+
export const getWorksPage = async ({
|
|
301
|
+
client,
|
|
302
|
+
cursor,
|
|
303
|
+
limit = 50,
|
|
304
|
+
maxPages,
|
|
305
|
+
}: {
|
|
306
|
+
client: DataClient;
|
|
307
|
+
cursor?: string;
|
|
308
|
+
limit?: number;
|
|
309
|
+
maxPages?: number;
|
|
310
|
+
}): Promise<WorksPage> => {
|
|
311
|
+
const clampedLimit = Math.min(limit, 200);
|
|
312
|
+
|
|
313
|
+
let query = client
|
|
314
|
+
.from('works')
|
|
315
|
+
.select(
|
|
316
|
+
`
|
|
317
|
+
uuid,
|
|
318
|
+
title,
|
|
319
|
+
description,
|
|
320
|
+
tohs:work_toh!inner(toh:toh_clean),
|
|
321
|
+
publicationDate,
|
|
322
|
+
publicationVersion,
|
|
323
|
+
pages:source_pages,
|
|
324
|
+
restriction,
|
|
325
|
+
breadcrumb
|
|
326
|
+
`,
|
|
327
|
+
{ count: 'exact' },
|
|
328
|
+
)
|
|
329
|
+
.not('toh', 'like', 'toh00%')
|
|
330
|
+
.order('title', { ascending: true })
|
|
331
|
+
.limit(clampedLimit + 1);
|
|
332
|
+
|
|
333
|
+
if (maxPages) {
|
|
334
|
+
query = query.lt('source_pages', maxPages);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (cursor) {
|
|
338
|
+
const { data: cursorWork } = await client
|
|
339
|
+
.from('works')
|
|
340
|
+
.select('title')
|
|
341
|
+
.eq('uuid', cursor)
|
|
342
|
+
.single();
|
|
343
|
+
|
|
344
|
+
if (cursorWork) {
|
|
345
|
+
query = query.gt('title', cursorWork.title);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const { data, error, count } = await query;
|
|
350
|
+
|
|
351
|
+
if (error) {
|
|
352
|
+
console.error('Error fetching works:', error);
|
|
353
|
+
return {
|
|
354
|
+
items: [],
|
|
355
|
+
pageInfo: {
|
|
356
|
+
nextCursor: null,
|
|
357
|
+
prevCursor: null,
|
|
358
|
+
hasMoreAfter: false,
|
|
359
|
+
hasMoreBefore: false,
|
|
360
|
+
},
|
|
361
|
+
totalCount: 0,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const hasMoreAfter = (data ?? []).length > clampedLimit;
|
|
366
|
+
const items = hasMoreAfter
|
|
367
|
+
? (data ?? []).slice(0, clampedLimit)
|
|
368
|
+
: (data ?? []);
|
|
369
|
+
const works = items.map((dto) => workFromDTO(dto as WorkDTO));
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
items: works,
|
|
373
|
+
pageInfo: {
|
|
374
|
+
nextCursor: hasMoreAfter ? (works[works.length - 1]?.uuid ?? null) : null,
|
|
375
|
+
prevCursor: cursor ?? null,
|
|
376
|
+
hasMoreAfter,
|
|
377
|
+
hasMoreBefore: Boolean(cursor),
|
|
378
|
+
},
|
|
379
|
+
totalCount: count ?? 0,
|
|
380
|
+
};
|
|
381
|
+
};
|