Skip to content

Commit 4d7c708

Browse files
committed
feat: endpoint to get solution group details
1 parent 3de1ec7 commit 4d7c708

5 files changed

Lines changed: 249 additions & 2 deletions

File tree

src/checks.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from './polkadot/polka';
99
import { sleep } from './util/sleep';
1010
import { getBaseUrls } from './util/base-urls';
11+
import { saveOperatorAddress } from './util/operator-address-cache';
1112

1213
export const runChecks = async (api: ApiPromise, account: KeyringPair, logger): Promise<void> => {
1314
const shouldRetryInfinite: boolean = MAIN_CONFIG.RETRY_WORKER_CHECKS;
@@ -54,6 +55,9 @@ export const performInitialChecks = async (
5455

5556
logger.info({ operatorAddress }, 'operator address');
5657

58+
// Save operator address to local cache
59+
saveOperatorAddress(account.address, operatorAddress);
60+
5761
const operatorSubscriptions: string[] = await retryHttpAsyncCall(
5862
async () => await getOperatorSubscriptions(api, operatorAddress),
5963
);

src/health/health.ts

Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { getAllInstalledSolutionsNames, runtimeStarted } from '../node-red/red';
1+
import {
2+
getAllInstalledSolutionsNames,
3+
getAllInstalledSolutionsWithGroups,
4+
runtimeStarted,
5+
type InstalledSolutionDetails,
6+
} from '../node-red/red';
7+
import { createKeyringPair } from '../polkadot/account';
8+
import { MAIN_CONFIG } from '../config';
9+
import { getCachedOperatorAddress } from '../util/operator-address-cache';
210

311
enum HealthStatus {
412
OK = 'OK',
@@ -23,6 +31,33 @@ interface NodeRedHealthStatus extends ComponentHealthStatus {
2331
};
2432
}
2533

34+
interface SolutionInfo {
35+
id: string;
36+
name: string;
37+
status: string;
38+
installed: boolean;
39+
}
40+
41+
interface SolutionGroupInfo {
42+
id: string;
43+
name?: string;
44+
hasCidAllowList?: boolean;
45+
solutions: SolutionInfo[];
46+
}
47+
48+
interface OperatorInfo {
49+
address: string;
50+
subscriptions: string[];
51+
solutionGroups: SolutionGroupInfo[];
52+
}
53+
54+
interface SolutionGroupsDetailsStatus {
55+
timestamp: string;
56+
rpcUrl?: string;
57+
workerAddress?: string;
58+
operator?: OperatorInfo;
59+
}
60+
2661
export const isLive = (): ComponentHealthStatus => {
2762
return {
2863
status: HealthStatus.OK,
@@ -59,3 +94,111 @@ export const getNodeRedHealth = async (): Promise<NodeRedHealthStatus> => {
5994
},
6095
};
6196
};
97+
98+
const getOperatorInfo = async (timeoutMs: number = 5000): Promise<OperatorInfo | undefined> => {
99+
try {
100+
const account = createKeyringPair();
101+
102+
// Get installed solutions with groups from Node-RED (local, fast)
103+
let installedSolutionsWithGroups: InstalledSolutionDetails[] = [];
104+
try {
105+
installedSolutionsWithGroups = await getAllInstalledSolutionsWithGroups();
106+
} catch {
107+
return undefined;
108+
}
109+
110+
if (installedSolutionsWithGroups.length === 0) {
111+
// No installed solutions - still need operator address
112+
const operatorAddress = await getCachedOperatorAddress(account.address, timeoutMs);
113+
114+
if (operatorAddress == null) {
115+
return undefined;
116+
}
117+
118+
return {
119+
address: operatorAddress,
120+
subscriptions: [],
121+
solutionGroups: [],
122+
};
123+
}
124+
125+
// Extract unique solution group IDs from installed solutions (local data)
126+
const uniqueSolutionGroupIds = [
127+
...new Set(installedSolutionsWithGroups.map((s) => s.solutionGroupId)),
128+
];
129+
130+
// Get operator address (required) - getCachedOperatorAddress handles cache and chain query
131+
const operatorAddress = await getCachedOperatorAddress(account.address, timeoutMs);
132+
133+
if (operatorAddress == null) {
134+
return undefined;
135+
}
136+
137+
// Group installed solutions by solutionGroupId (local data)
138+
const solutionGroupsMap = new Map<string, InstalledSolutionDetails[]>();
139+
for (const solution of installedSolutionsWithGroups) {
140+
const group = solutionGroupsMap.get(solution.solutionGroupId) ?? [];
141+
group.push(solution);
142+
solutionGroupsMap.set(solution.solutionGroupId, group);
143+
}
144+
145+
// Build solution groups from local data
146+
const solutionGroups: SolutionGroupInfo[] = Array.from(solutionGroupsMap.entries()).map(
147+
([solutionGroupId, solutions]) => {
148+
const solutionInfos: SolutionInfo[] = solutions.map((solution) => {
149+
// Extract name from solutionId (format: "name.uuid")
150+
// Example: "newTestGPSaaS.1348d595-ccc4-4a38-85ad-f0e31cc7f410" -> "newTestGPSaaS"
151+
const name = solution.solutionId.includes('.')
152+
? solution.solutionId.split('.').slice(0, -1).join('.')
153+
: solution.solutionId;
154+
155+
return {
156+
id: solution.solutionId,
157+
name,
158+
status: 'Active', // Assume active if installed
159+
installed: true, // All are installed
160+
};
161+
});
162+
163+
return {
164+
id: solutionGroupId,
165+
name: solutionGroupId, // Use groupId as name (we don't have name from chain)
166+
solutions: solutionInfos,
167+
};
168+
},
169+
);
170+
171+
return {
172+
address: operatorAddress,
173+
subscriptions: uniqueSolutionGroupIds,
174+
solutionGroups,
175+
};
176+
} catch {
177+
return undefined;
178+
}
179+
};
180+
181+
export const getSolutionGroupsDetailsStatus = async (): Promise<SolutionGroupsDetailsStatus> => {
182+
const timestamp = new Date().toISOString();
183+
184+
// Gather configuration (non-sensitive)
185+
let rpcUrl: string | undefined;
186+
let workerAddress: string | undefined;
187+
try {
188+
const account = createKeyringPair();
189+
workerAddress = account.address;
190+
rpcUrl = MAIN_CONFIG.PALLET_RPC_URL;
191+
} catch {
192+
rpcUrl = MAIN_CONFIG.PALLET_RPC_URL;
193+
}
194+
195+
// Get operator information with solution groups
196+
const operatorInfo = await getOperatorInfo();
197+
198+
return {
199+
timestamp,
200+
rpcUrl,
201+
workerAddress,
202+
operator: operatorInfo,
203+
};
204+
};

src/node-red/red.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,33 @@ export const getAllInstalledSolutionsNames = async (): Promise<string[]> => {
398398
return solutionIds.filter((x) => x !== null);
399399
};
400400

401+
export interface InstalledSolutionDetails {
402+
solutionId: string;
403+
solutionGroupId: string;
404+
}
405+
406+
export const getAllInstalledSolutionsWithGroups = async (): Promise<InstalledSolutionDetails[]> => {
407+
const tabNodes = await getTabNodes();
408+
409+
const solutions: Array<InstalledSolutionDetails | null> = await Promise.all(
410+
tabNodes.map(async (tabNode: RedNode) => {
411+
const solutionId = getNodeEnv(tabNode, 'EWX_SOLUTION_ID', false);
412+
const solutionGroupId = getNodeEnv(tabNode, 'EWX_SOLUTION_GROUP_ID', false);
413+
414+
if (solutionId == null || solutionGroupId == null) {
415+
return null;
416+
}
417+
418+
return {
419+
solutionId,
420+
solutionGroupId,
421+
};
422+
}),
423+
);
424+
425+
return solutions.filter((x): x is InstalledSolutionDetails => x !== null);
426+
};
427+
401428
export const getTabNodes = async (): Promise<RedNodes> => {
402429
const currentFlows: Flows = await getAllFlows();
403430

src/routes/health.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { MAIN_CONFIG } from '../config';
22
import express from 'express';
33
import { createLogger } from '../util/logger';
4-
import { isLive, isReady } from '../health/health';
4+
import { isLive, isReady, getSolutionGroupsDetailsStatus } from '../health/health';
55
import asyncHandler from 'express-async-handler';
66

77
export const createHealthRouter = (): express.Router | null => {
@@ -32,5 +32,16 @@ export const createHealthRouter = (): express.Router | null => {
3232
}),
3333
);
3434

35+
healthRouter.get(
36+
'/health/status',
37+
asyncHandler(async (req, res) => {
38+
const result = await getSolutionGroupsDetailsStatus();
39+
40+
healthLogger.debug(result, 'requested solution groups details status');
41+
42+
res.json(result);
43+
}),
44+
);
45+
3546
return healthRouter;
3647
};

src/util/operator-address-cache.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { createLogger } from './logger';
2+
import { getOperatorAddress, retryHttpAsyncCall } from '../polkadot/polka';
3+
import { createReadPalletApi } from './pallet-api';
4+
5+
const logger = createLogger('OperatorAddressCache');
6+
7+
// In-memory cache: Map<workerAddress, operatorAddress>
8+
const operatorAddressCache = new Map<string, string>();
9+
10+
export const saveOperatorAddress = (workerAddress: string, operatorAddress: string): void => {
11+
operatorAddressCache.set(workerAddress, operatorAddress);
12+
};
13+
14+
export const getCachedOperatorAddress = async (
15+
workerAddress: string,
16+
timeoutMs: number = 5000,
17+
): Promise<string | null> => {
18+
// Return cached value if available
19+
const cached = operatorAddressCache.get(workerAddress);
20+
if (cached != null) {
21+
return cached;
22+
}
23+
24+
// Query chain if not cached
25+
try {
26+
const apiPromise = retryHttpAsyncCall(async () => await createReadPalletApi());
27+
const timeoutPromise = new Promise<never>((_resolve, reject) => {
28+
setTimeout(() => {
29+
reject(new Error('Operator address fetch timeout'));
30+
}, timeoutMs);
31+
});
32+
33+
const api = await Promise.race([apiPromise, timeoutPromise]);
34+
35+
const operatorAddressPromise = retryHttpAsyncCall(
36+
async () => await getOperatorAddress(api, workerAddress),
37+
);
38+
const operatorAddress = await Promise.race([operatorAddressPromise, timeoutPromise]).catch(
39+
() => null,
40+
);
41+
42+
await api.disconnect().catch(() => {
43+
// Ignore disconnect errors
44+
});
45+
46+
if (operatorAddress != null) {
47+
// Cache the result
48+
operatorAddressCache.set(workerAddress, operatorAddress);
49+
return operatorAddress;
50+
}
51+
52+
logger.warn({ workerAddress }, 'no operator assigned to worker');
53+
return null;
54+
} catch (error) {
55+
logger.warn({ error, workerAddress }, 'failed to get operator address from chain');
56+
return null;
57+
}
58+
};
59+
60+
export const clearOperatorAddress = (workerAddress: string): void => {
61+
operatorAddressCache.delete(workerAddress);
62+
};

0 commit comments

Comments
 (0)