Skip to content

Commit 4e8001a

Browse files
authored
Add getEnrichedLinksTree action (#3211)
1 parent 4e42f43 commit 4e8001a

File tree

3 files changed

+216
-1
lines changed

3 files changed

+216
-1
lines changed

src/shared/schema/mix/actions/entries.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ import type {
1010
SwitchPublicationStatusResponse,
1111
} from '../../us/types';
1212
import {
13+
buildEnrichedLinksTree,
1314
checkEntriesForPublication,
15+
collectAllRelatedEntryIds,
1416
escapeStringForLike,
17+
fetchEntriesWithLinks,
1518
getEntryMetaStatusByError,
1619
} from '../helpers';
1720
import {isValidPublishLink} from '../helpers/validation';
@@ -20,6 +23,8 @@ import type {
2023
DeleteEntryResponse,
2124
GetBatchEntriesByIdsArgs,
2225
GetBatchEntriesByIdsResponse,
26+
GetEnrichedLinksTreeArgs,
27+
GetEnrichedLinksTreeResponse,
2328
GetEntriesInFolderArgs,
2429
GetEntriesInFolderResponse,
2530
GetEntryMetaStatusArgs,
@@ -237,4 +242,39 @@ export const entriesActions = {
237242
return {entries: entriesResponse.entries};
238243
},
239244
),
245+
getEnrichedLinksTree: createAction<GetEnrichedLinksTreeResponse, GetEnrichedLinksTreeArgs>(
246+
async (api, {entryId}, {ctx}) => {
247+
const typedApi = getTypedApi(api);
248+
249+
const allRelatedEntryIds = await collectAllRelatedEntryIds({
250+
entryId,
251+
typedApi,
252+
ctx,
253+
});
254+
255+
const entriesData = await fetchEntriesWithLinks({
256+
entryIds: Array.from(allRelatedEntryIds),
257+
typedApi,
258+
ctx,
259+
});
260+
261+
let annotations;
262+
if (allRelatedEntryIds.size > 0) {
263+
try {
264+
annotations = await typedApi.us.getEntriesAnnotation({
265+
entryIds: Array.from(allRelatedEntryIds),
266+
});
267+
} catch (error) {
268+
ctx.logError('Error getting entries annotation', error as Error);
269+
}
270+
}
271+
272+
const linksTree = buildEnrichedLinksTree({
273+
entriesData,
274+
annotations,
275+
});
276+
277+
return {linksTree};
278+
},
279+
),
240280
};

src/shared/schema/mix/helpers/entries.ts

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type {
88
CheckDatasetsForPublicationResponse,
99
GetEntryMetaStatusResponse,
1010
} from '../../types';
11-
import type {EntryFields} from '../../us/types';
11+
import type {EntryFields, GetEntriesEntryResponse} from '../../us/types';
1212

1313
export function filterEntirsForCheck(entries: Pick<EntryFields, 'entryId' | 'scope'>[]) {
1414
const datasetIds: string[] = [];
@@ -116,3 +116,157 @@ export function getEntryMetaStatusByError(errorWrapper: unknown): GetEntryMetaSt
116116
return {code: 'UNHANDLED'};
117117
}
118118
}
119+
120+
export async function collectAllRelatedEntryIds({
121+
entryId,
122+
typedApi,
123+
ctx,
124+
}: {
125+
entryId: string;
126+
typedApi: TypedApi;
127+
ctx?: {logError: (message: string, error: Error) => void};
128+
}): Promise<Set<string>> {
129+
const allRelatedEntryIds = new Set<string>();
130+
const visited = new Set<string>();
131+
132+
async function collectRelations(currentEntryId: string): Promise<void> {
133+
if (visited.has(currentEntryId)) {
134+
return;
135+
}
136+
visited.add(currentEntryId);
137+
138+
try {
139+
const relations = await typedApi.us.getRelations({
140+
entryId: currentEntryId,
141+
direction: 'parent',
142+
});
143+
144+
for (const relation of relations) {
145+
allRelatedEntryIds.add(relation.entryId);
146+
await collectRelations(relation.entryId);
147+
}
148+
} catch (error) {
149+
ctx?.logError(`Error getting relations for entry ${currentEntryId}`, error as Error);
150+
}
151+
}
152+
153+
await collectRelations(entryId);
154+
155+
return allRelatedEntryIds;
156+
}
157+
158+
export async function fetchEntriesWithLinks({
159+
entryIds,
160+
typedApi,
161+
ctx,
162+
}: {
163+
entryIds: string[];
164+
typedApi: TypedApi;
165+
ctx?: {logError: (message: string, error: Error) => void};
166+
}): Promise<(GetEntriesEntryResponse | null)[]> {
167+
if (entryIds.length === 0) {
168+
return [];
169+
}
170+
171+
const results: (GetEntriesEntryResponse | null)[] = [];
172+
173+
// Process in batches of 50 to avoid URL length limitations
174+
for (let i = 0; i < entryIds.length; i += 50) {
175+
const batchIds = entryIds.slice(i, i + 50);
176+
177+
try {
178+
const entriesResult = await typedApi.us.getEntries({
179+
ids: batchIds,
180+
includeLinks: true,
181+
});
182+
183+
// Create map for quick lookup
184+
const entriesMap = new Map(
185+
entriesResult.entries.map((entry) => [entry.entryId, entry]),
186+
);
187+
188+
// Preserve order from batchIds
189+
batchIds.forEach((id) => {
190+
const entry = entriesMap.get(id);
191+
results.push(entry || null);
192+
});
193+
} catch (error) {
194+
ctx?.logError(`Error fetching entries batch (${batchIds.length} IDs)`, error as Error);
195+
results.push(...new Array(batchIds.length).fill(null));
196+
}
197+
}
198+
199+
return results;
200+
}
201+
202+
export function buildEnrichedLinksTree({
203+
entriesData,
204+
annotations,
205+
}: {
206+
entriesData: (GetEntriesEntryResponse | null)[];
207+
annotations?: Array<{
208+
entryId: string;
209+
result?: {
210+
scope?: EntryScope;
211+
type?: string;
212+
annotation?: {
213+
description?: string;
214+
};
215+
};
216+
error?: {
217+
code?: string;
218+
message?: string;
219+
details?: unknown;
220+
};
221+
}>;
222+
}) {
223+
const linksTree: Record<string, {description?: string; links: Record<string, any>}> = {};
224+
225+
// Build basic tree structure
226+
for (const entry of entriesData) {
227+
if (!entry) continue;
228+
229+
linksTree[entry.entryId] = {
230+
links: {},
231+
};
232+
233+
if ('links' in entry && entry.links) {
234+
for (const [_linkId, linkEntryId] of Object.entries(entry.links)) {
235+
if (typeof linkEntryId === 'string') {
236+
linksTree[entry.entryId].links[linkEntryId] = {
237+
entryId: linkEntryId,
238+
links: {},
239+
};
240+
}
241+
}
242+
}
243+
}
244+
245+
// Enrich with annotations if provided
246+
if (annotations) {
247+
const annotationsMap = new Map(
248+
annotations
249+
.filter(
250+
(ann): ann is typeof ann & {result: NonNullable<typeof ann.result>} =>
251+
'result' in ann && Boolean(ann.result),
252+
)
253+
.map((ann) => [ann.entryId, ann.result.annotation?.description]),
254+
);
255+
256+
for (const [currentEntryId, node] of Object.entries(linksTree)) {
257+
const description = annotationsMap.get(currentEntryId);
258+
if (description) {
259+
node.description = description;
260+
}
261+
262+
for (const [linkId, linkNode] of Object.entries(node.links)) {
263+
const linkDescription = annotationsMap.get(linkId);
264+
if (linkDescription) {
265+
linkNode.description = linkDescription;
266+
}
267+
}
268+
}
269+
}
270+
271+
return linksTree;
272+
}

src/shared/schema/mix/types/entries.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,24 @@ export type GetEntriesInFolderResponse = EntryByKeyPattern[];
102102
export type GetBatchEntriesByIdsArgs = Omit<GetEntriesArgs, 'ids'> & {ids: string[]};
103103

104104
export type GetBatchEntriesByIdsResponse = Pick<GetEntriesResponse, 'entries'>;
105+
106+
export interface EnrichedLinkNode {
107+
entryId: string;
108+
description?: string;
109+
links: Record<string, EnrichedLinkNode>;
110+
}
111+
112+
export interface EnrichedLinksTree {
113+
[entryId: string]: {
114+
description?: string;
115+
links: Record<string, EnrichedLinkNode>;
116+
};
117+
}
118+
119+
export interface GetEnrichedLinksTreeResponse {
120+
linksTree: EnrichedLinksTree;
121+
}
122+
123+
export interface GetEnrichedLinksTreeArgs {
124+
entryId: string;
125+
}

0 commit comments

Comments
 (0)