"use strict"; // ================================================================ // AMPP Folder Placer — lib/ampp-folder-placer.js // // placeAsset() — original v3.5 port (filename -- parsing) // placeAssetInFolderById() — direct placement by known folder ID // getOrCreateFolderByPath() — hierarchy lookup + create, returns folder ID // listAmppFolders() — fetch AMPP virtual folder tree (probes endpoints) // amppRequest() — shared low-level REST helper // ================================================================ const PREFIX_DELIM = "--"; // ── Low-level REST helper ───────────────────────────────────────────────────── async function amppRequest(method, baseUrl, token, apiPath, body = null) { const url = `${baseUrl.replace(/\/$/, "")}/${apiPath}`; const opts = { method, headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, signal: AbortSignal.timeout(15000), }; if (body !== null) { opts.body = typeof body === "string" ? body : JSON.stringify(body); } const r = await fetch(url, opts); if (!r.ok) { const text = await r.text().catch(() => ""); throw new Error( `AMPP API ${method} ${apiPath} → HTTP ${r.status}: ${text.slice(0, 200)}` ); } return r.json(); } // ── Get or create a folder by slash-separated path ─────────────────────────── // e.g. "NEWS/PACKAGES" → looks up hierarchy, creates missing levels, returns ID. async function getOrCreateFolderByPath(baseUrl, folderPath, token) { const parts = folderPath.split("/").map(s => s.trim()).filter(Boolean); if (!parts.length) throw new Error("folderPath is empty"); let parentId = null; let currentPath = ""; let expectedDepth = 0; for (const fname of parts) { currentPath = currentPath ? `${currentPath}/${fname}` : fname; expectedDepth++; let folderId = null; // Try hierarchy lookup first try { const encoded = currentPath.split("/").map(encodeURIComponent).join("/"); const resp = await amppRequest("GET", baseUrl, token, `api/v1/store/folder/folders/hierarchy?path=${encoded}`); const hlist = resp?.["hierarchy:list"]; if (Array.isArray(hlist) && hlist.length === expectedDepth) { const last = hlist[hlist.length - 1]; const candidateId = (last?.["folder:id"] ?? "").toString().trim(); if (candidateId) folderId = candidateId; } } catch (err) { console.warn(`[AMPP] Hierarchy lookup for '${currentPath}': ${err.message}`); } // Create the folder if not found if (!folderId) { const createBody = { "name:text": fname }; if (parentId) createBody["parentFolders:tags"] = [parentId]; const fresp = await amppRequest("POST", baseUrl, token, "api/v1/store/folder/folders", createBody); const createdId = (fresp?.["folder:id"] ?? "").toString().trim(); if (!createdId) throw new Error(`Folder create response missing folder:id for '${fname}'`); folderId = createdId; } parentId = folderId; } return parentId; // ID of the deepest folder } // ── List AMPP virtual folders ──────────────────────────────────────────────── // Probes multiple candidate endpoints in order; uses the first that responds. // Returns: { folders: [{id, name, parentId, path}], endpoint } async function listAmppFolders(baseUrl, token) { let lastError = null; // Helper: normalise raw array of folder objects function normaliseFolders(raw) { if (!Array.isArray(raw)) return null; const folders = raw.map((f) => ({ id: (f["folder:id"] ?? f.id ?? "").toString().trim(), name: (f["name:text"] ?? f.name ?? f["folder:name"] ?? "").toString().trim(), parentId: (f["parentFolder:id"] ?? f.parentId ?? f["parent:id"] ?? "").toString().trim(), path: (f["folder:path"] ?? f.path ?? "").toString().trim(), })).filter(f => f.id && f.name); return folders; } // 1. Try POST querypage (same pattern as the jobs API) const queryPagePaths = [ "api/v1/store/folder/folders/querypage?skip=0&limit=500&sort=name:text&asc=true", "api/v1/store/folder/folders/querypage", ]; for (const candidate of queryPagePaths) { try { const resp = await amppRequest("POST", baseUrl, token, candidate, "{}"); const raw = Array.isArray(resp) ? resp : (resp?.["folder:list"] ?? resp?.items ?? resp?.results ?? resp?.folders ?? []); const folders = normaliseFolders(raw); if (folders && folders.length > 0) { console.log(`[AMPP folders] ${folders.length} folders via POST ${candidate}`); return { folders, endpoint: `POST ${candidate}` }; } } catch (err) { lastError = err; console.warn(`[AMPP folders] POST ${candidate} failed: ${err.message}`); } } // 2. Try GET candidates const getCandidates = [ "api/v1/store/folder/folders?limit=500", "api/v1/store/folder/folders", "api/v1/store/folder/folder/list", "api/v1/store/folder/folders/list", ]; for (const candidate of getCandidates) { try { const resp = await amppRequest("GET", baseUrl, token, candidate); const raw = Array.isArray(resp) ? resp : (resp?.["folder:list"] ?? resp?.folders ?? resp?.items ?? resp?.results ?? []); const folders = normaliseFolders(raw); if (folders && folders.length > 0) { console.log(`[AMPP folders] ${folders.length} folders via GET ${candidate}`); return { folders, endpoint: candidate }; } } catch (err) { lastError = err; console.warn(`[AMPP folders] GET ${candidate} failed: ${err.message}`); } } // 3. Hierarchy walk: call hierarchy with empty path to probe root-level folders try { const resp = await amppRequest("GET", baseUrl, token, "api/v1/store/folder/folders/hierarchy?path="); const hlist = resp?.["hierarchy:list"] ?? resp?.["folder:list"]; if (Array.isArray(hlist) && hlist.length > 0) { const folders = normaliseFolders(hlist); if (folders && folders.length > 0) { console.log(`[AMPP folders] ${folders.length} root folders via hierarchy walk`); return { folders, endpoint: "hierarchy-root" }; } } } catch (err) { lastError = err; console.warn(`[AMPP folders] hierarchy root probe failed: ${err.message}`); } // All endpoints failed — caller should fall back to manual path entry throw new Error( `Could not list AMPP folders — no list endpoint available. ${lastError?.message ?? ""}` ); } // ── Direct placement by known folder ID ────────────────────────────────────── async function placeAssetInFolderById(baseUrl, assetId, folderId, token) { if (!assetId) throw new Error("assetId is required"); if (!folderId) throw new Error("folderId is required"); const linkBody = JSON.stringify( { "folder:id": folderId, "asset:id": assetId }, null, 0 ); await amppRequest("POST", baseUrl, token, "api/v1/store/folder/references", linkBody); return { placed: true, folderId, assetId }; } // ── Original v3.5 placement (filename -- parsing) ──────────────────────────── async function placeAsset(baseUrl, assetName, assetId, token) { if (!assetName.includes(PREFIX_DELIM)) { return { placed: false, folderId: null, path: "", reason: `no "${PREFIX_DELIM}" delimiter in filename` }; } const parts = assetName.split(PREFIX_DELIM); if (parts.length < 2) { return { placed: false, folderId: null, path: "", reason: "fewer than 2 parts after split" }; } const folderNames = parts.slice(0, -1); const folderPath = folderNames.map(s => s.trim()).filter(Boolean).join("/"); const folderId = await getOrCreateFolderByPath(baseUrl, folderPath, token); if (folderId) { const linkBody = JSON.stringify({ "folder:id": folderId, "asset:id": assetId }, null, 0); await amppRequest("POST", baseUrl, token, "api/v1/store/folder/references", linkBody); } return { placed: true, folderId, path: folderPath }; } module.exports = { placeAsset, placeAssetInFolderById, getOrCreateFolderByPath, listAmppFolders, amppRequest };