fix: folder listing — add POST querypage candidate + getOrCreateFolderByPath helper
This commit is contained in:
parent
4ee422a5e8
commit
1be810be24
1 changed files with 127 additions and 84 deletions
|
|
@ -3,10 +3,11 @@
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// AMPP Folder Placer — lib/ampp-folder-placer.js
|
// AMPP Folder Placer — lib/ampp-folder-placer.js
|
||||||
//
|
//
|
||||||
// placeAsset() — original v3.5 port (filename -- parsing)
|
// placeAsset() — original v3.5 port (filename -- parsing)
|
||||||
// placeAssetInFolderById() — direct placement by known folder ID
|
// placeAssetInFolderById() — direct placement by known folder ID
|
||||||
// listAmppFolders() — fetch AMPP virtual folder tree (probes endpoints)
|
// getOrCreateFolderByPath() — hierarchy lookup + create, returns folder ID
|
||||||
// amppRequest() — shared low-level REST helper
|
// listAmppFolders() — fetch AMPP virtual folder tree (probes endpoints)
|
||||||
|
// amppRequest() — shared low-level REST helper
|
||||||
// ================================================================
|
// ================================================================
|
||||||
|
|
||||||
const PREFIX_DELIM = "--";
|
const PREFIX_DELIM = "--";
|
||||||
|
|
@ -35,54 +36,138 @@ async function amppRequest(method, baseUrl, token, apiPath, body = null) {
|
||||||
return r.json();
|
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 ────────────────────────────────────────────────
|
// ── List AMPP virtual folders ────────────────────────────────────────────────
|
||||||
// Probes multiple candidate endpoints in order; uses the first that responds.
|
// Probes multiple candidate endpoints in order; uses the first that responds.
|
||||||
// Returns: { folders: [{id, name, parentId, path}], endpoint }
|
// Returns: { folders: [{id, name, parentId, path}], endpoint }
|
||||||
const FOLDER_LIST_CANDIDATES = [
|
|
||||||
"api/v1/store/folder/folders?limit=500",
|
|
||||||
"api/v1/store/folder/folders",
|
|
||||||
"api/v1/store/folder/folder/list",
|
|
||||||
"api/v1/store/folder/folders/list",
|
|
||||||
];
|
|
||||||
|
|
||||||
async function listAmppFolders(baseUrl, token) {
|
async function listAmppFolders(baseUrl, token) {
|
||||||
let lastError = null;
|
let lastError = null;
|
||||||
|
|
||||||
for (const candidate of FOLDER_LIST_CANDIDATES) {
|
// 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 {
|
try {
|
||||||
const resp = await amppRequest("GET", baseUrl, token, candidate);
|
const resp = await amppRequest("POST", baseUrl, token, candidate, "{}");
|
||||||
|
const raw = Array.isArray(resp) ? resp
|
||||||
// Normalise whatever shape AMPP returns into an array
|
: (resp?.["folder:list"] ?? resp?.items ?? resp?.results ?? resp?.folders ?? []);
|
||||||
const raw = Array.isArray(resp)
|
const folders = normaliseFolders(raw);
|
||||||
? resp
|
if (folders && folders.length > 0) {
|
||||||
: resp?.["folder:list"] ?? resp?.folders ?? resp?.items ?? resp?.results ?? [];
|
console.log(`[AMPP folders] ${folders.length} folders via POST ${candidate}`);
|
||||||
|
return { folders, endpoint: `POST ${candidate}` };
|
||||||
if (!Array.isArray(raw)) {
|
|
||||||
console.warn(`[AMPP folders] ${candidate} returned non-array: ${typeof raw}`);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
console.log(`[AMPP folders] ${folders.length} folders via ${candidate}`);
|
|
||||||
return { folders, endpoint: candidate };
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
lastError = err;
|
lastError = err;
|
||||||
console.warn(`[AMPP folders] ${candidate} failed: ${err.message}`);
|
console.warn(`[AMPP folders] POST ${candidate} failed: ${err.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Could not list AMPP folders — all endpoints failed. Last error: ${lastError?.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 ──────────────────────────────────────
|
// ── Direct placement by known folder ID ──────────────────────────────────────
|
||||||
// Used by the background placement worker. No filename parsing — we already
|
|
||||||
// know the folder ID from what the user selected in the UI.
|
|
||||||
async function placeAssetInFolderById(baseUrl, assetId, folderId, token) {
|
async function placeAssetInFolderById(baseUrl, assetId, folderId, token) {
|
||||||
if (!assetId) throw new Error("assetId is required");
|
if (!assetId) throw new Error("assetId is required");
|
||||||
if (!folderId) throw new Error("folderId is required");
|
if (!folderId) throw new Error("folderId is required");
|
||||||
|
|
@ -96,7 +181,6 @@ async function placeAssetInFolderById(baseUrl, assetId, folderId, token) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Original v3.5 placement (filename -- parsing) ────────────────────────────
|
// ── Original v3.5 placement (filename -- parsing) ────────────────────────────
|
||||||
// Kept for the manual /api/ampp/place endpoint.
|
|
||||||
async function placeAsset(baseUrl, assetName, assetId, token) {
|
async function placeAsset(baseUrl, assetName, assetId, token) {
|
||||||
if (!assetName.includes(PREFIX_DELIM)) {
|
if (!assetName.includes(PREFIX_DELIM)) {
|
||||||
return { placed: false, folderId: null, path: "", reason: `no "${PREFIX_DELIM}" delimiter in filename` };
|
return { placed: false, folderId: null, path: "", reason: `no "${PREFIX_DELIM}" delimiter in filename` };
|
||||||
|
|
@ -108,57 +192,16 @@ async function placeAsset(baseUrl, assetName, assetId, token) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const folderNames = parts.slice(0, -1);
|
const folderNames = parts.slice(0, -1);
|
||||||
let parentId = null;
|
const folderPath = folderNames.map(s => s.trim()).filter(Boolean).join("/");
|
||||||
let currentPath = "";
|
|
||||||
let expectedDepth = 0;
|
|
||||||
|
|
||||||
for (const rawName of folderNames) {
|
const folderId = await getOrCreateFolderByPath(baseUrl, folderPath, token);
|
||||||
const fname = rawName.trim();
|
|
||||||
if (!fname) continue;
|
|
||||||
|
|
||||||
currentPath = currentPath ? `${currentPath}/${fname}` : fname;
|
if (folderId) {
|
||||||
expectedDepth++;
|
const linkBody = JSON.stringify({ "folder:id": folderId, "asset:id": assetId }, null, 0);
|
||||||
let folderId = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const encoded = currentPath.split("/").map(encodeURIComponent).join("/");
|
|
||||||
const resp = await amppRequest("GET", baseUrl, token, `api/v1/store/folder/folders/hierarchy?path=${encoded}`);
|
|
||||||
if (!resp || typeof resp !== "object" || Array.isArray(resp)) throw new Error(`Hierarchy resp is not an object: ${typeof resp}`);
|
|
||||||
const hlist = resp["hierarchy:list"];
|
|
||||||
if (!Array.isArray(hlist)) throw new Error(`hierarchy:list is not an array: ${typeof hlist}`);
|
|
||||||
if (hlist.length === expectedDepth) {
|
|
||||||
const last = hlist[hlist.length - 1];
|
|
||||||
const candidateId = (last?.["folder:id"] ?? "").toString().trim();
|
|
||||||
if (candidateId) folderId = candidateId;
|
|
||||||
}
|
|
||||||
} catch (lookupEx) {
|
|
||||||
folderId = null;
|
|
||||||
console.warn(`[AMPP folder] Hierarchy lookup failed for '${currentPath}': ${lookupEx.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!folderId) {
|
|
||||||
const createBody = { "name:text": fname };
|
|
||||||
if (parentId) createBody["parentFolders:tags"] = [parentId];
|
|
||||||
try {
|
|
||||||
const fresp = await amppRequest("POST", baseUrl, token, "api/v1/store/folder/folders", createBody);
|
|
||||||
if (!fresp || typeof fresp !== "object" || Array.isArray(fresp)) throw new Error(`Folder create resp is not an object: ${typeof fresp}`);
|
|
||||||
const createdId = (fresp["folder:id"] ?? "").toString().trim();
|
|
||||||
if (!createdId) throw new Error("Folder create resp missing folder:id");
|
|
||||||
folderId = createdId;
|
|
||||||
} catch (ex) {
|
|
||||||
throw new Error(`Failed to create folder '${fname}' under path '${currentPath}': ${ex.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parentId = folderId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parentId) {
|
|
||||||
const linkBody = JSON.stringify({ "folder:id": parentId, "asset:id": assetId }, null, 0);
|
|
||||||
await amppRequest("POST", baseUrl, token, "api/v1/store/folder/references", linkBody);
|
await amppRequest("POST", baseUrl, token, "api/v1/store/folder/references", linkBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { placed: true, folderId: parentId, path: currentPath };
|
return { placed: true, folderId, path: folderPath };
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { placeAsset, placeAssetInFolderById, listAmppFolders, amppRequest };
|
module.exports = { placeAsset, placeAssetInFolderById, getOrCreateFolderByPath, listAmppFolders, amppRequest };
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue