@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,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
|
+
};
|
|
@@ -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
|
+
};
|