"use strict"; const { requireAuth, pool } = require("./context"); const ASSET_TYPES = new Set(["character", "scene", "prop", "video", "image", "asset", "other"]); const ASSET_STATUSES = new Set(["ready", "draft", "reviewing", "pending", "failed"]); function cleanText(value, maxLength) { return String(value || "").trim().slice(0, maxLength); } function safeJsonString(value, fallback) { if (value === undefined) return JSON.stringify(fallback); try { return JSON.stringify(value ?? fallback); } catch { return JSON.stringify(fallback); } } function parseJson(value, fallback) { if (!value || typeof value !== "string") return fallback; try { return JSON.parse(value); } catch { return fallback; } } function normalizeTags(value) { return Array.isArray(value) ? value.map((item) => cleanText(item, 40)).filter(Boolean).slice(0, 20) : []; } function normalizeAssetPayload(body = {}, partial = false) { const name = cleanText(body.name ?? body.title, 200); if (!partial && !name) return { error: "Missing asset name" }; const type = cleanText(body.type ?? body.assetType, 32); const status = cleanText(body.status, 32); const sourceTaskId = Number(body.sourceTaskId ?? body.source_task_id); return { value: { name: name || undefined, type: ASSET_TYPES.has(type) ? type : partial ? undefined : "asset", description: cleanText(body.description, 5000) || null, url: cleanText(body.url ?? body.imageUrl, 1000) || null, ossKey: cleanText(body.ossKey ?? body.oss_key, 500) || null, tagsJson: safeJsonString(normalizeTags(body.tags), []), status: ASSET_STATUSES.has(status) ? status : partial ? undefined : "ready", sourceTaskId: Number.isFinite(sourceTaskId) ? sourceTaskId : null, sourceProjectId: cleanText(body.sourceProjectId ?? body.projectId ?? body.source_project_id, 64) || null, metadataJson: safeJsonString(body.metadata, {}), }, }; } function formatAsset(row) { return { id: Number(row.id), type: row.type, name: row.name, description: row.description || "", url: row.url || null, ossKey: row.oss_key || null, imageUrl: row.url || "", tags: parseJson(row.tags_json, []), status: row.status, sourceTaskId: row.source_task_id == null ? null : String(row.source_task_id), sourceProjectId: row.source_project_id || null, metadata: parseJson(row.metadata_json, {}), createdAt: row.created_at, updatedAt: row.updated_at, }; } function registerAssetRoutes(router) { router.get("/assets", requireAuth, async (req, res) => { try { const type = cleanText(req.query.type, 32); const status = cleanText(req.query.status, 32); const q = cleanText(req.query.q, 120).toLowerCase(); const limit = Math.min(Math.max(Number(req.query.limit) || 100, 1), 200); const params = [req.user.id]; const where = ["user_id = $1"]; if (ASSET_TYPES.has(type)) { params.push(type); where.push(`type = $${params.length}`); } if (ASSET_STATUSES.has(status)) { params.push(status); where.push(`status = $${params.length}`); } if (q) { params.push(`%${q}%`); where.push(`(LOWER(name) LIKE $${params.length} OR LOWER(COALESCE(description, '')) LIKE $${params.length} OR LOWER(tags_json) LIKE $${params.length})`); } params.push(limit); const { rows } = await pool.query( ` SELECT * FROM web_assets WHERE ${where.join(" AND ")} ORDER BY updated_at DESC LIMIT $${params.length} `, params, ); res.json({ assets: rows.map(formatAsset) }); } catch (err) { console.error("[assets] list failed:", err.message); res.status(500).json({ error: "Failed to load assets" }); } }); router.post("/assets", requireAuth, async (req, res) => { const payload = normalizeAssetPayload(req.body || {}); if (payload.error) return res.status(400).json({ error: payload.error }); try { const asset = payload.value; const { rows } = await pool.query( ` INSERT INTO web_assets ( user_id, type, name, description, url, oss_key, tags_json, status, source_task_id, source_project_id, metadata_json ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING * `, [ req.user.id, asset.type, asset.name, asset.description, asset.url, asset.ossKey, asset.tagsJson, asset.status, asset.sourceTaskId, asset.sourceProjectId, asset.metadataJson, ], ); res.status(201).json({ asset: formatAsset(rows[0]) }); } catch (err) { console.error("[assets] create failed:", err.message); res.status(500).json({ error: "Failed to create asset" }); } }); router.patch("/assets/:id", requireAuth, async (req, res) => { const payload = normalizeAssetPayload(req.body || {}, true); if (payload.error) return res.status(400).json({ error: payload.error }); const fieldMap = { name: "name", type: "type", description: "description", url: "url", ossKey: "oss_key", tagsJson: "tags_json", status: "status", sourceTaskId: "source_task_id", sourceProjectId: "source_project_id", metadataJson: "metadata_json", }; const values = []; const updates = []; Object.entries(payload.value).forEach(([key, value]) => { if (value === undefined) return; values.push(value); updates.push(`${fieldMap[key]} = $${values.length}`); }); if (!updates.length) return res.status(400).json({ error: "No asset fields to update" }); values.push(req.params.id, req.user.id); try { const { rows } = await pool.query( ` UPDATE web_assets SET ${updates.join(", ")}, updated_at = NOW() WHERE id = $${values.length - 1} AND user_id = $${values.length} RETURNING * `, values, ); if (!rows[0]) return res.status(404).json({ error: "Asset not found" }); res.json({ asset: formatAsset(rows[0]) }); } catch (err) { console.error("[assets] update failed:", err.message); res.status(500).json({ error: "Failed to update asset" }); } }); router.delete("/assets/:id", requireAuth, async (req, res) => { try { const { rowCount } = await pool.query("DELETE FROM web_assets WHERE id = $1 AND user_id = $2", [ req.params.id, req.user.id, ]); if (!rowCount) return res.status(404).json({ error: "Asset not found" }); res.json({ success: true }); } catch (err) { console.error("[assets] delete failed:", err.message); res.status(500).json({ error: "Failed to delete asset" }); } }); } module.exports = { registerAssetRoutes };