217 lines
6.8 KiB
JavaScript
217 lines
6.8 KiB
JavaScript
"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 };
|