Initial commit: OmniAI backend server
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
"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 };
|
||||
Reference in New Issue
Block a user