UNPKG

@seqera/node-red-seqera

Version:

Node-RED nodes for interacting with the Seqera Platform API

182 lines (157 loc) 6.9 kB
const axios = require("axios"); const { apiCall } = require("./_utils"); /** * Helper to evaluate a typed-input property value, supporting JSONata. * Returns a Promise resolving to the evaluated value. */ async function evalProp(RED, node, msg, value, type) { if (type === "jsonata") { const expr = RED.util.prepareJSONataExpression(value, node); return new Promise((resolve, reject) => { RED.util.evaluateJSONataExpression(expr, msg, (err, res) => { if (err) return reject(err); resolve(res); }); }); } return RED.util.evaluateNodeProperty(value, type, node, msg); } /** * Resolve a Seqera Platform Data Link by name. Returns an object containing * the IDs and metadata needed by other helper functions. */ async function resolveDataLink(RED, node, msg, dataLinkName, { baseUrl, workspaceId = null } = {}) { if (!dataLinkName) { throw new Error("dataLinkName not provided"); } const dlSearchQS = new URLSearchParams(); if (workspaceId != null) dlSearchQS.append("workspaceId", workspaceId); dlSearchQS.append("pageSize", "2"); dlSearchQS.append("search", dataLinkName); const searchUrl = `${baseUrl.replace(/\/$/, "")}/data-links/?${dlSearchQS.toString()}`; const searchResp = await apiCall(node, "get", searchUrl, { headers: { Accept: "application/json" } }); const links = searchResp.data?.dataLinks || []; if (!links.length) throw new Error(`Could not find Data Link '${dataLinkName}'`); if (links.length !== 1) throw new Error(`Found more than one Data Link matching '${dataLinkName}'`); const link = links[0]; return { dataLinkId: link.id, credentialsId: link.credentials?.[0]?.id, resourceRef: link.resourceRef, resourceType: link.type, provider: link.provider, }; } /** * Core implementation that lists objects from a Seqera Platform Data Link. * * The logic relies on `node` having the same property names as the existing * `seqera-datalink-list` node (eg. `dataLinkNameProp`, `workspaceIdProp` etc). * * Returns an object `{ items, files }` where `items` is an array of objects * and `files` is an array containing just the names. */ async function listDataLink(RED, node, msg = {}) { // Evaluate all configurable inputs const dataLinkName = await evalProp(RED, node, msg, node.dataLinkNameProp, node.dataLinkNamePropType); const basePathRaw = await evalProp(RED, node, msg, node.basePathProp, node.basePathPropType); const prefix = await evalProp(RED, node, msg, node.prefixProp, node.prefixPropType); const patternRaw = await evalProp(RED, node, msg, node.patternProp, node.patternPropType); const maxResultsRaw = await evalProp(RED, node, msg, node.maxResultsProp, node.maxResultsPropType); const workspaceIdOv = await evalProp(RED, node, msg, node.workspaceIdProp, node.workspaceIdPropType); const baseUrlOv = await evalProp(RED, node, msg, node.baseUrlProp, node.baseUrlPropType); const depthRaw = await evalProp(RED, node, msg, node.depthProp, node.depthPropType); if (!dataLinkName) { throw new Error("dataLinkName not provided"); } const depthVal = Math.max(0, parseInt(depthRaw, 10) || 0); const maxResults = parseInt(maxResultsRaw, 10) || 100; const basePath = basePathRaw != null ? String(basePathRaw) : ""; const baseUrl = baseUrlOv || (node.seqeraConfig && node.seqeraConfig.baseUrl) || node.defaultBaseUrl; const workspaceId = workspaceIdOv || (node.seqeraConfig && node.seqeraConfig.workspaceId) || null; // Build auth headers const headersBase = { Accept: "application/json" }; const build = () => headersBase; /* ----------------------------------------------------------- * 1) Resolve Data Link ID & credentials * --------------------------------------------------------- */ const { dataLinkId, credentialsId, resourceRef, resourceType, provider } = await resolveDataLink( RED, node, msg, dataLinkName, { baseUrl, workspaceId, }, ); /* ----------------------------------------------------------- * 2) Recursive browse of paths (depth & pagination aware) * --------------------------------------------------------- */ const allItems = []; const fetchPath = async (currentPath, currentDepth) => { if (allItems.length >= maxResults) return; let nextPage = null; do { const encodedPath = currentPath.split("/").filter(Boolean).map(encodeURIComponent).join("/"); let url = `${baseUrl.replace(/\/$/, "")}/data-links/${encodeURIComponent(dataLinkId)}/browse/${encodedPath}`; const qs = new URLSearchParams(); if (workspaceId != null) qs.append("workspaceId", workspaceId); if (prefix) qs.append("search", prefix); if (credentialsId) qs.append("credentialsId", credentialsId); if (nextPage) qs.append("nextPageToken", nextPage); const qsStr = qs.toString(); if (qsStr) url += `?${qsStr}`; const resp = await apiCall(node, "get", url, { headers: build() }); const objects = Array.isArray(resp.data?.objects) ? resp.data.objects : []; if (objects.length) { const mapped = objects.map((o) => { const p = currentPath ? `${currentPath}/` : ""; return { ...o, name: `${p}${o.name}` }; }); const remaining = maxResults - allItems.length; allItems.push(...mapped.slice(0, remaining)); } // recurse into folders if depth allows if (currentDepth < depthVal && allItems.length < maxResults) { const folders = objects.filter((o) => (o.type || "").toUpperCase() === "FOLDER"); for (const folder of folders) { if (allItems.length >= maxResults) break; const clean = folder.name.replace(/\/$/, ""); const newPath = currentPath ? `${currentPath}/${clean}` : clean; await fetchPath(newPath, currentDepth + 1); } } nextPage = resp.data.nextPageToken || resp.data.nextPage || null; } while (nextPage && allItems.length < maxResults); }; await fetchPath(basePath, 0); /* ----------------------------------------------------------- * 3) Post-processing filters * --------------------------------------------------------- */ let finalItems = allItems; if (patternRaw) { try { const re = new RegExp(patternRaw); finalItems = allItems.filter((it) => re.test(it.name)); } catch (e) { node.warn(`Invalid regex pattern: ${patternRaw}`); } } if (node.returnType === "files") { finalItems = finalItems.filter((it) => (it.type || "").toUpperCase() === "FILE"); } else if (node.returnType === "folders") { finalItems = finalItems.filter((it) => (it.type || "").toUpperCase() === "FOLDER"); } return { items: finalItems, files: finalItems.map((f) => f.name), resourceType, resourceRef, provider, }; } module.exports = { listDataLink, resolveDataLink, };