@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
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export * from './lib/auth';
|
|
2
2
|
export * from './lib/bibliography';
|
|
3
|
-
export * from './lib/canon';
|
|
4
3
|
export * from './lib/client-browser';
|
|
5
4
|
export * from './lib/client-server';
|
|
6
5
|
export * from './lib/client-token';
|
|
@@ -10,8 +9,8 @@ export * from './lib/imprint';
|
|
|
10
9
|
export * from './lib/panel-url';
|
|
11
10
|
export * from './lib/library';
|
|
12
11
|
export * from './lib/passage';
|
|
13
|
-
export * from './lib/projects';
|
|
14
12
|
export * from './lib/publications';
|
|
13
|
+
export * from './lib/replace';
|
|
15
14
|
export * from './lib/storage';
|
|
16
15
|
export * from './lib/types';
|
|
17
16
|
export * from './lib/local-storage';
|
package/src/lib/folio.ts
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
'use server';
|
|
2
2
|
|
|
3
3
|
import { createServerClient } from './client-ssr';
|
|
4
|
-
import { TohokuCatalogEntry } from './types';
|
|
4
|
+
import { DataClient, TohokuCatalogEntry } from './types';
|
|
5
5
|
import { FolioDTO, folioFromDTO } from './types/folio';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
toh,
|
|
10
|
-
page = 0,
|
|
11
|
-
size = 10,
|
|
12
|
-
}: {
|
|
7
|
+
type GetWorkFoliosArgs = {
|
|
8
|
+
client: DataClient;
|
|
13
9
|
uuid: string;
|
|
14
10
|
toh: TohokuCatalogEntry;
|
|
15
11
|
page?: number;
|
|
16
12
|
size?: number;
|
|
17
|
-
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const getWorkFolios = async ({
|
|
16
|
+
client,
|
|
17
|
+
uuid,
|
|
18
|
+
toh,
|
|
19
|
+
page = 0,
|
|
20
|
+
size = 10,
|
|
21
|
+
}: GetWorkFoliosArgs) => {
|
|
18
22
|
const start = page * size;
|
|
19
23
|
const end = start + size - 1;
|
|
20
|
-
const client = await createServerClient();
|
|
21
24
|
const { data, error } = await client
|
|
22
25
|
.from('tibetan_works_folios')
|
|
23
26
|
.select(
|
|
@@ -30,6 +33,7 @@ export const getFolios = async ({
|
|
|
30
33
|
)
|
|
31
34
|
.eq('work_uuid', uuid)
|
|
32
35
|
.eq('toh', toh)
|
|
36
|
+
.order('volume_number', { ascending: true })
|
|
33
37
|
.order('folio_number', { ascending: true })
|
|
34
38
|
.order('side', { ascending: true })
|
|
35
39
|
.range(start, end);
|
|
@@ -41,3 +45,13 @@ export const getFolios = async ({
|
|
|
41
45
|
|
|
42
46
|
return data.map((dto) => folioFromDTO(dto as FolioDTO));
|
|
43
47
|
};
|
|
48
|
+
|
|
49
|
+
export const getFolios = async ({
|
|
50
|
+
uuid,
|
|
51
|
+
toh,
|
|
52
|
+
page = 0,
|
|
53
|
+
size = 10,
|
|
54
|
+
}: Omit<GetWorkFoliosArgs, 'client'>) => {
|
|
55
|
+
const client = await createServerClient();
|
|
56
|
+
return getWorkFolios({ client, uuid, toh, page, size });
|
|
57
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { DataClient } from '../types';
|
|
2
|
+
|
|
3
|
+
export const getGlossaryDisplayNamesByUuids = async ({
|
|
4
|
+
client,
|
|
5
|
+
glossaryUuids,
|
|
6
|
+
}: {
|
|
7
|
+
client: DataClient;
|
|
8
|
+
glossaryUuids: readonly string[];
|
|
9
|
+
}): Promise<Map<string, string>> => {
|
|
10
|
+
const namesByUuid = new Map<string, string>();
|
|
11
|
+
if (glossaryUuids.length === 0) return namesByUuid;
|
|
12
|
+
|
|
13
|
+
const { data, error } = await client
|
|
14
|
+
.from('glossaries')
|
|
15
|
+
.select('uuid, names:names!name_uuid(content)')
|
|
16
|
+
.in('uuid', glossaryUuids as string[]);
|
|
17
|
+
|
|
18
|
+
if (error) {
|
|
19
|
+
console.error('Error batch loading glossary names:', error);
|
|
20
|
+
return namesByUuid;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const glossary of data ?? []) {
|
|
24
|
+
const names = glossary.names as unknown as
|
|
25
|
+
| { content: string }
|
|
26
|
+
| { content: string }[]
|
|
27
|
+
| null;
|
|
28
|
+
const content = Array.isArray(names) ? names[0]?.content : names?.content;
|
|
29
|
+
if (content) {
|
|
30
|
+
namesByUuid.set(glossary.uuid, content);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return namesByUuid;
|
|
35
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DataClient,
|
|
3
|
+
GlossaryTermInstanceDTO,
|
|
4
|
+
GlossaryTermInstancesDTO,
|
|
5
|
+
glossaryTermInstanceFromDTO,
|
|
6
|
+
} from '../types';
|
|
7
|
+
|
|
8
|
+
export const getGlossaryInstances = async ({
|
|
9
|
+
client,
|
|
10
|
+
uuid,
|
|
11
|
+
withAttestations = false,
|
|
12
|
+
}: {
|
|
13
|
+
client: DataClient;
|
|
14
|
+
uuid: string;
|
|
15
|
+
withAttestations?: boolean;
|
|
16
|
+
}) => {
|
|
17
|
+
const { data, error } = await client.rpc('show_glossary_entries', {
|
|
18
|
+
v_work_uuid: uuid,
|
|
19
|
+
v_with_attestation: withAttestations,
|
|
20
|
+
});
|
|
21
|
+
if (error) {
|
|
22
|
+
console.error(
|
|
23
|
+
`Error fetching glossary instances for work: ${uuid} `,
|
|
24
|
+
error,
|
|
25
|
+
);
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const dto = data as GlossaryTermInstancesDTO;
|
|
30
|
+
return dto.glossary_entries.map(glossaryTermInstanceFromDTO).sort((a, b) => {
|
|
31
|
+
const nameA = a.names.english || '';
|
|
32
|
+
const nameB = b.names.english || '';
|
|
33
|
+
return nameA.localeCompare(nameB);
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const getGlossaryInstance = async ({
|
|
38
|
+
client,
|
|
39
|
+
uuid,
|
|
40
|
+
}: {
|
|
41
|
+
client: DataClient;
|
|
42
|
+
uuid: string;
|
|
43
|
+
}) => {
|
|
44
|
+
const { data, error } = await client.rpc('show_glossary_entry', {
|
|
45
|
+
v_glossary_uuid: uuid,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (error) {
|
|
49
|
+
console.error(`Error fetching glossary instance: ${uuid} `, error);
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return glossaryTermInstanceFromDTO(data as GlossaryTermInstanceDTO);
|
|
54
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DataClient,
|
|
3
|
+
GlossaryLandingItem,
|
|
4
|
+
GlossaryLandingItemDTO,
|
|
5
|
+
glossaryLandingItemFromDTO,
|
|
6
|
+
} from '../types';
|
|
7
|
+
|
|
8
|
+
export const getAllGlossaryTerms = async ({
|
|
9
|
+
client,
|
|
10
|
+
uuids,
|
|
11
|
+
}: {
|
|
12
|
+
client: DataClient;
|
|
13
|
+
uuids?: string[];
|
|
14
|
+
}): Promise<GlossaryLandingItem[]> => {
|
|
15
|
+
const pageSize = 1000;
|
|
16
|
+
let start = 0;
|
|
17
|
+
let end = pageSize - 1;
|
|
18
|
+
let count = pageSize;
|
|
19
|
+
const terms: GlossaryLandingItem[] = [];
|
|
20
|
+
|
|
21
|
+
while (count === pageSize) {
|
|
22
|
+
let rpc = client.rpc('scholar_glossary_get_all');
|
|
23
|
+
|
|
24
|
+
if (uuids?.length) {
|
|
25
|
+
rpc = rpc.in('authority_uuid', uuids);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { data, error } = await rpc.range(start, end);
|
|
29
|
+
|
|
30
|
+
if (error) {
|
|
31
|
+
console.error('Error fetching glossary terms:', error);
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
if (!data || !data.length) {
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const dtos = data as GlossaryLandingItemDTO[];
|
|
39
|
+
const items = dtos
|
|
40
|
+
.map((item) => glossaryLandingItemFromDTO(item as GlossaryLandingItemDTO))
|
|
41
|
+
.flatMap((item) => (item ? [item] : []));
|
|
42
|
+
|
|
43
|
+
terms.push(...items);
|
|
44
|
+
start += pageSize;
|
|
45
|
+
end += pageSize;
|
|
46
|
+
count = data.length;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return terms;
|
|
50
|
+
};
|
|
@@ -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
|
+
};
|