334 lines
22 KiB
JavaScript
334 lines
22 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { createHash } from "node:crypto";
|
|
import { access, mkdir, rm, writeFile } from "node:fs/promises";
|
|
import { resolve } from "node:path";
|
|
import { dbGet, dbRun } from "../server/db.mjs";
|
|
|
|
const api = process.env.AI_DRAMA_API_BASE || "http://127.0.0.1:8787";
|
|
const projectRoot = resolve(import.meta.dirname, "..");
|
|
const organizationId = "org-studio-lab";
|
|
const workspaceId = "ws-local-aidrama";
|
|
const runId = Date.now();
|
|
const projectId = `smoke-portal-project-${runId}`;
|
|
const sourceRoot = `storage/smoke-delivery-portal-${runId}`;
|
|
const sourcePath = `${sourceRoot}/clip.mp4`;
|
|
const lastFramePath = `${sourceRoot}/actual-last-frame.jpg`;
|
|
const manifestPath = `${sourceRoot}/manifest.json`;
|
|
const deliveryVersion = `portal-${runId}`;
|
|
const deliveryDraftVersion = `portal-draft-${runId}`;
|
|
const batchId = `smoke-portal-batch-${runId}`;
|
|
const deliveryLabel = `smoke-portal-delivery-${runId}`;
|
|
const draftDeliveryLabel = `smoke-portal-draft-${runId}`;
|
|
const customerId = `cust-portal-${runId}`;
|
|
const contractId = `contract-portal-${runId}`;
|
|
const auditTargetIds = new Set();
|
|
const accessLinkIds = [];
|
|
const releaseIds = [];
|
|
const deliveryIds = [];
|
|
|
|
async function request(path, options = {}) {
|
|
const response = await fetch(`${api}${path}`, {
|
|
...options,
|
|
headers: { "content-type": "application/json", ...(options.headers || {}) }
|
|
});
|
|
const payload = await response.json().catch(() => ({}));
|
|
return { response, payload };
|
|
}
|
|
|
|
async function requestBinary(path) {
|
|
const response = await fetch(`${api}${path}`);
|
|
return { response, body: Buffer.from(await response.arrayBuffer()), contentType: response.headers.get("content-type") || "" };
|
|
}
|
|
|
|
function expectStatus(result, status, label) {
|
|
assert.equal(result.response.status, status, `${label}: ${result.response.status} ${result.payload?.detail || result.payload?.error || ""}`);
|
|
return result.payload;
|
|
}
|
|
|
|
function expectOk(result, label) {
|
|
assert.equal(result.response.ok, true, `${label}: ${result.response.status} ${result.payload?.detail || result.payload?.error || ""}`);
|
|
return result.payload;
|
|
}
|
|
|
|
function isoFromNow(days) {
|
|
return new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString();
|
|
}
|
|
|
|
const producerLogin = expectOk(await request("/api/auth/login", {
|
|
method: "POST",
|
|
body: JSON.stringify({ email: "producer@local.test", password: "Demo@123456" })
|
|
}), "producer login");
|
|
const reviewerLogin = expectOk(await request("/api/auth/login", {
|
|
method: "POST",
|
|
body: JSON.stringify({ email: "review@local.test", password: "Demo@123456" })
|
|
}), "reviewer login");
|
|
|
|
const producerSetupHeaders = {
|
|
authorization: `Bearer ${producerLogin.session.token}`,
|
|
"x-organization-id": organizationId,
|
|
"x-workspace-id": workspaceId,
|
|
"x-project-id": "thunder-mouth"
|
|
};
|
|
const producerHeaders = {
|
|
authorization: `Bearer ${producerLogin.session.token}`,
|
|
"x-organization-id": organizationId,
|
|
"x-workspace-id": workspaceId,
|
|
"x-project-id": projectId
|
|
};
|
|
const reviewerHeaders = {
|
|
authorization: `Bearer ${reviewerLogin.session.token}`,
|
|
"x-organization-id": organizationId,
|
|
"x-workspace-id": workspaceId,
|
|
"x-project-id": projectId
|
|
};
|
|
const crossOrganizationHeaders = {
|
|
authorization: `Bearer ${producerLogin.session.token}`,
|
|
"x-organization-id": "org-northstar",
|
|
"x-workspace-id": "ws-northstar-main",
|
|
"x-project-id": "northstar-pilot"
|
|
};
|
|
|
|
const producerUser = dbGet("SELECT id FROM users WHERE email = ?", ["producer@local.test"]);
|
|
assert.ok(producerUser?.id, "producer user must exist");
|
|
|
|
function seedContractClearance() {
|
|
const timestamp = new Date().toISOString();
|
|
dbRun(
|
|
"INSERT OR IGNORE INTO customers(id, organization_id, name, legal_name, code, customer_type, status, industry, region, billing_email, tax_id, notes, tags_json, metadata_json, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 'distributor', 'active', 'AI 短剧发行', 'CN', 'portal@example.local', '', 'delivery portal smoke customer', '[\"smoke\",\"portal\"]', '{}', ?, ?, ?)",
|
|
[customerId, organizationId, `门户烟测客户 ${runId}`, `门户烟测客户 ${runId} 有限公司`, `PORTAL-${runId}`, producerUser.id, timestamp, timestamp]
|
|
);
|
|
dbRun(
|
|
"INSERT OR IGNORE INTO production_contracts(id, organization_id, customer_id, contract_number, title, contract_type, status, approval_status, effective_at, expires_at, signed_at, currency, amount, license_scope_json, rights_json, delivery_terms_json, evidence_json, risk_json, approval_note, created_by, updated_by, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 'production_license', 'active', 'approved', ?, ?, ?, 'CNY', 0, ?, ?, ?, ?, ?, 'smoke portal contract', ?, ?, ?, ?)",
|
|
[
|
|
contractId,
|
|
organizationId,
|
|
customerId,
|
|
`PORTAL-CTR-${runId}`,
|
|
`门户烟测授权 ${runId}`,
|
|
new Date(Date.now() - 86400000).toISOString(),
|
|
new Date(Date.now() + 365 * 86400000).toISOString(),
|
|
timestamp,
|
|
JSON.stringify({ channels: ["local-file", "private-delivery-portal"], territories: ["CN"], deliverables: ["vertical-video", "delivery-manifest"], platforms: ["private-preview", "internal-review"], exclusive: false, commercialUse: true, language: ["zh-CN"] }),
|
|
JSON.stringify({ originalStory: true, derivativeProduction: true, aiGeneratedAssetsAllowed: true, voiceCloneAllowed: false, fixedVoiceRequired: true, singleFramePolicyRequired: true }),
|
|
JSON.stringify({ customerPortalAllowed: true, requireClearanceCertificate: true, maxAccessDays: 7 }),
|
|
JSON.stringify({ contractRef: `smoke://portal-contract/${runId}`, rightsEvidenceRef: `smoke://portal-rights/${runId}`, signedCopyPath: `storage/contracts/smoke-portal/${runId}.json` }),
|
|
JSON.stringify({ paidCloudAllowed: false, publicModelAllowed: false, rightsReview: "passed" }),
|
|
producerUser.id,
|
|
producerUser.id,
|
|
timestamp,
|
|
timestamp
|
|
]
|
|
);
|
|
dbRun(
|
|
"INSERT OR IGNORE INTO contract_project_bindings(id, organization_id, workspace_id, project_id, contract_id, customer_id, usage_mode, status, notes, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, 'primary-license', 'active', 'delivery portal smoke binding', ?, ?, ?)",
|
|
[`contract-binding-portal-${runId}`, organizationId, workspaceId, projectId, contractId, customerId, producerUser.id, timestamp, timestamp]
|
|
);
|
|
}
|
|
|
|
let channelId = "";
|
|
let publishedReleaseId = "";
|
|
let primaryToken = "";
|
|
let expiredToken = "";
|
|
let revokedToken = "";
|
|
let seeded = false;
|
|
let projectCreated = false;
|
|
|
|
try {
|
|
const project = expectStatus(await request("/api/projects", {
|
|
method: "POST",
|
|
headers: producerSetupHeaders,
|
|
body: JSON.stringify({ id: projectId, name: `门户烟测项目 ${runId}`, type: "AI 漫剧" })
|
|
}), 201, "producer creates isolated portal project");
|
|
projectCreated = true;
|
|
assert.equal(project.project.id, projectId, "isolated portal project must be created");
|
|
const graph = expectOk(await request("/api/production/graph", { headers: producerHeaders }), "read isolated portal graph");
|
|
const projectShot = { id: graph.graph.shots[0]?.id };
|
|
assert.ok(projectShot.id, "smoke project must have a starter shot");
|
|
|
|
const channels = expectOk(await request("/api/production/delivery-channels", { headers: reviewerHeaders }), "reviewer can view delivery channels");
|
|
channelId = channels.channels.find((channel) => channel.kind === "local-file")?.id || "";
|
|
assert.ok(channelId, "workspace must expose a local-file delivery channel");
|
|
expectStatus(await request(`/api/production/releases/unknown/access-links`, { method: "POST", headers: reviewerHeaders, body: JSON.stringify({}) }), 403, "reviewer cannot create access link");
|
|
|
|
const draft = expectStatus(await request("/api/production/deliveries", {
|
|
method: "POST",
|
|
headers: producerHeaders,
|
|
body: JSON.stringify({ version: deliveryDraftVersion, channel: draftDeliveryLabel })
|
|
}), 201, "producer creates unpublished delivery");
|
|
deliveryIds.push(draft.delivery.id);
|
|
const draftRelease = expectStatus(await request(`/api/production/deliveries/${encodeURIComponent(draft.delivery.id)}/releases`, {
|
|
method: "POST",
|
|
headers: producerHeaders,
|
|
body: JSON.stringify({ channelId, submit: false })
|
|
}), 201, "producer creates unpublished release draft");
|
|
releaseIds.push(draftRelease.release.id);
|
|
|
|
const delivery = expectStatus(await request("/api/production/deliveries", {
|
|
method: "POST",
|
|
headers: producerHeaders,
|
|
body: JSON.stringify({ version: deliveryVersion, channel: deliveryLabel })
|
|
}), 201, "producer creates published delivery draft");
|
|
deliveryIds.push(delivery.delivery.id);
|
|
|
|
await mkdir(resolve(projectRoot, sourceRoot), { recursive: true });
|
|
await writeFile(resolve(projectRoot, sourcePath), Buffer.from("fake local video evidence"));
|
|
await writeFile(resolve(projectRoot, lastFramePath), Buffer.from("fake actual last frame evidence"));
|
|
await writeFile(resolve(projectRoot, manifestPath), JSON.stringify({ schema: "smoke-delivery-portal-manifest", deliveryId: delivery.delivery.id }, null, 2));
|
|
const timestamp = new Date().toISOString();
|
|
dbRun("INSERT INTO delivery_batches(id, organization_id, workspace_id, project_id, delivery_id, batch_number, label, status, manifest_path, source_json, result_json, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 1, 'smoke portal batch', 'active', ?, '{}', ?, ?, ?, ?)", [batchId, organizationId, workspaceId, projectId, delivery.delivery.id, manifestPath, JSON.stringify({ blockers: [], manifestWritten: true }), producerUser.id, timestamp, timestamp]);
|
|
dbRun("INSERT INTO delivery_batch_items(id, batch_id, shot_id, sequence_number, source_path, actual_last_frame_path, source_sha256, metadata_json, created_at) VALUES (?, ?, ?, 1, ?, ?, ?, ?, ?)", [`smoke-portal-item-${runId}`, batchId, projectShot.id, sourcePath, lastFramePath, "a".repeat(64), JSON.stringify({ artifactStatus: "inspected" }), timestamp]);
|
|
dbRun("UPDATE deliveries SET status = 'approved', active_batch_id = ?, approved_by = ?, approved_at = ?, updated_at = ? WHERE id = ?", [batchId, producerUser.id, timestamp, timestamp, delivery.delivery.id]);
|
|
expectOk(await request("/api/production/reviews", { headers: producerHeaders }), "ensure portal QA reviews");
|
|
dbRun("UPDATE reviews SET status = 'approved', score = 100, decision_by = ?, decision_at = ?, evidence_json = ?, updated_at = ? WHERE project_id = ?", [producerUser.id, timestamp, JSON.stringify({ source: "smoke-delivery-portal", checkedAt: timestamp }), timestamp, projectId]);
|
|
seedContractClearance();
|
|
seeded = true;
|
|
|
|
const release = expectStatus(await request(`/api/production/deliveries/${encodeURIComponent(delivery.delivery.id)}/releases`, {
|
|
method: "POST",
|
|
headers: producerHeaders,
|
|
body: JSON.stringify({ channelId, submit: true, idempotencyKey: `portal-release-${runId}` })
|
|
}), 201, "producer submits portal release");
|
|
publishedReleaseId = release.release.id;
|
|
releaseIds.push(publishedReleaseId);
|
|
const approved = expectOk(await request(`/api/production/releases/${encodeURIComponent(publishedReleaseId)}/decision`, {
|
|
method: "POST",
|
|
headers: producerHeaders,
|
|
body: JSON.stringify({ status: "approved", note: "portal smoke approval" })
|
|
}), "producer approves portal release");
|
|
assert.equal(approved.release.status, "approved", "release must be approved before publishing");
|
|
const published = expectOk(await request(`/api/production/releases/${encodeURIComponent(publishedReleaseId)}/publish`, { method: "POST", headers: producerHeaders, body: "{}" }), "producer publishes portal release");
|
|
assert.equal(published.release.status, "published", "release must be published");
|
|
await access(resolve(projectRoot, published.release.output_path));
|
|
|
|
const unpublishedLinkAttempt = await request(`/api/production/releases/${encodeURIComponent(draftRelease.release.id)}/access-links`, {
|
|
method: "POST",
|
|
headers: producerHeaders,
|
|
body: JSON.stringify({ maxDownloads: 1 })
|
|
});
|
|
expectStatus(unpublishedLinkAttempt, 409, "unpublished release cannot be shared");
|
|
|
|
const reviewerList = expectOk(await request(`/api/production/releases/${encodeURIComponent(publishedReleaseId)}/access-links`, { headers: reviewerHeaders }), "reviewer can list access links");
|
|
assert.deepEqual(reviewerList.accessLinks, [], "new release must have no access links");
|
|
expectStatus(await request(`/api/production/releases/${encodeURIComponent(publishedReleaseId)}/access-links`, {
|
|
method: "POST",
|
|
headers: reviewerHeaders,
|
|
body: JSON.stringify({ maxDownloads: 1 })
|
|
}), 403, "reviewer cannot create access link");
|
|
|
|
const link = expectStatus(await request(`/api/production/releases/${encodeURIComponent(publishedReleaseId)}/access-links`, {
|
|
method: "POST",
|
|
headers: producerHeaders,
|
|
body: JSON.stringify({ recipientName: "星河发行部", recipientEmail: "release@example.test", expiresAt: isoFromNow(2), maxDownloads: 2 })
|
|
}), 201, "producer creates primary access link");
|
|
primaryToken = link.token;
|
|
accessLinkIds.push(link.link.id);
|
|
auditTargetIds.add(link.link.id);
|
|
assert.ok(primaryToken, "create response must return plaintext token once");
|
|
assert.match(link.portalUrl, /\/portal\//, "create response must return portal URL");
|
|
const storedLink = dbGet("SELECT token_hash, token_hint FROM delivery_access_links WHERE id = ?", [link.link.id]);
|
|
assert.notEqual(storedLink.token_hash, primaryToken, "database must not store plaintext token");
|
|
assert.equal(storedLink.token_hash, createHash("sha256").update(primaryToken).digest("hex"), "stored token hash must match SHA-256");
|
|
assert.ok(storedLink.token_hint && !storedLink.token_hint.includes(primaryToken), "stored token hint must not contain the full token");
|
|
|
|
const publicPortal = expectOk(await request(`/api/public/delivery/${encodeURIComponent(primaryToken)}`, { headers: { "user-agent": "portal-smoke" } }), "public portal metadata works");
|
|
assert.equal(publicPortal.delivery.version, deliveryVersion, "portal must identify the published delivery version");
|
|
assert.equal(publicPortal.portal.downloadsRemaining, 2, "portal must expose the initial download budget");
|
|
assert.equal(publicPortal.review.status, "pending", "portal must start in a pending client review state");
|
|
assert.ok(!JSON.stringify(publicPortal).includes("storage/"), "public metadata must not expose internal storage paths");
|
|
const changesRequested = expectOk(await request(`/api/public/delivery/${encodeURIComponent(primaryToken)}/feedback`, {
|
|
method: "POST",
|
|
headers: { "user-agent": "portal-feedback-smoke" },
|
|
body: JSON.stringify({ decision: "changes_requested", reviewerName: "星河发行部", reviewerEmail: "release@example.test", message: "请复核第 02 镜头的字幕安全区。" })
|
|
}), "client can request delivery changes");
|
|
assert.equal(changesRequested.review.status, "changes_requested", "portal must expose the latest client change request");
|
|
assert.equal(changesRequested.review.count, 1, "portal must count client feedback submissions");
|
|
const approvedFeedback = expectOk(await request(`/api/public/delivery/${encodeURIComponent(primaryToken)}/feedback`, {
|
|
method: "POST",
|
|
headers: { "user-agent": "portal-feedback-smoke" },
|
|
body: JSON.stringify({ decision: "approved", reviewerName: "星河发行部", reviewerEmail: "release@example.test", message: "已确认当前交付版本。" })
|
|
}), "client can accept delivery");
|
|
assert.equal(approvedFeedback.review.status, "approved", "portal must expose the latest client acceptance");
|
|
assert.equal(approvedFeedback.review.count, 2, "portal must retain the append-only feedback count");
|
|
expectStatus(await request(`/api/public/delivery/${encodeURIComponent(primaryToken)}/feedback`, {
|
|
method: "POST",
|
|
headers: { "user-agent": "portal-feedback-smoke" },
|
|
body: JSON.stringify({ decision: "changes_requested", message: "" })
|
|
}), 400, "change request without a message must be rejected");
|
|
const internalFeedback = expectOk(await request(`/api/production/releases/${encodeURIComponent(publishedReleaseId)}/access-feedback`, { headers: reviewerHeaders }), "internal users can inspect client feedback");
|
|
assert.equal(internalFeedback.feedback.length, 2, "internal feedback ledger must expose both client decisions");
|
|
assert.equal(internalFeedback.feedback[0].decision, "approved", "internal feedback must be newest first");
|
|
const releaseFile = await requestBinary(`/api/public/delivery/${encodeURIComponent(primaryToken)}/file?kind=release`);
|
|
assert.equal(releaseFile.response.status, 200, "public release file must download");
|
|
assert.match(releaseFile.contentType, /application\/json/, "release file must be JSON");
|
|
assert.match(releaseFile.body.toString("utf8"), /ai-drama-platform\.delivery-release\.v1/, "release JSON must be delivered");
|
|
const manifestFile = await requestBinary(`/api/public/delivery/${encodeURIComponent(primaryToken)}/file?kind=manifest`);
|
|
assert.equal(manifestFile.response.status, 200, "public manifest file must download");
|
|
assert.match(manifestFile.body.toString("utf8"), /smoke-delivery-portal-manifest/, "manifest JSON must be delivered");
|
|
const limitedDownload = await requestBinary(`/api/public/delivery/${encodeURIComponent(primaryToken)}/file?kind=release`);
|
|
assert.equal(limitedDownload.response.status, 429, "download budget must be enforced");
|
|
|
|
const expired = expectStatus(await request(`/api/production/releases/${encodeURIComponent(publishedReleaseId)}/access-links`, {
|
|
method: "POST",
|
|
headers: producerHeaders,
|
|
body: JSON.stringify({ recipientName: "过期链接", expiresAt: isoFromNow(2), maxDownloads: 1 })
|
|
}), 201, "producer creates expiring access link");
|
|
expiredToken = expired.token;
|
|
accessLinkIds.push(expired.link.id);
|
|
auditTargetIds.add(expired.link.id);
|
|
dbRun("UPDATE delivery_access_links SET expires_at = ?, status = 'active' WHERE id = ?", ["2000-01-01T00:00:00.000Z", expired.link.id]);
|
|
expectStatus(await request(`/api/public/delivery/${encodeURIComponent(expiredToken)}`, { headers: { "user-agent": "portal-smoke" } }), 410, "expired link must return 410");
|
|
|
|
const revoked = expectStatus(await request(`/api/production/releases/${encodeURIComponent(publishedReleaseId)}/access-links`, {
|
|
method: "POST",
|
|
headers: producerHeaders,
|
|
body: JSON.stringify({ recipientName: "撤销链接", expiresAt: isoFromNow(2), maxDownloads: 1 })
|
|
}), 201, "producer creates revocable access link");
|
|
revokedToken = revoked.token;
|
|
accessLinkIds.push(revoked.link.id);
|
|
auditTargetIds.add(revoked.link.id);
|
|
expectStatus(await request(`/api/production/access-links/${encodeURIComponent(revoked.link.id)}/revoke`, { method: "POST", headers: reviewerHeaders, body: "{}" }), 403, "reviewer cannot revoke access link");
|
|
expectOk(await request(`/api/production/access-links/${encodeURIComponent(revoked.link.id)}/revoke`, { method: "POST", headers: producerHeaders, body: "{}" }), "producer revokes access link");
|
|
expectStatus(await request(`/api/public/delivery/${encodeURIComponent(revokedToken)}`, { headers: { "user-agent": "portal-smoke" } }), 404, "revoked link must return 404");
|
|
|
|
const crossOrganization = await request(`/api/production/releases/${encodeURIComponent(publishedReleaseId)}/access-links`, { headers: crossOrganizationHeaders });
|
|
assert.ok([403, 404].includes(crossOrganization.response.status), `cross organization access links must be hidden or denied: ${crossOrganization.response.status}`);
|
|
|
|
const eventCount = dbGet("SELECT COUNT(*) AS count FROM delivery_access_events WHERE link_id IN (?, ?, ?)", accessLinkIds);
|
|
assert.ok(Number(eventCount?.count || 0) >= 6, "portal view/download/rejection activity must be audited");
|
|
console.log(`delivery portal smoke passed: ${publishedReleaseId} / ${accessLinkIds.length} links`);
|
|
} finally {
|
|
dbRun("DELETE FROM contract_license_events WHERE contract_id = ? OR customer_id = ? OR project_id = ?", [contractId, customerId, projectId]);
|
|
dbRun("DELETE FROM contract_project_bindings WHERE contract_id = ? OR project_id = ?", [contractId, projectId]);
|
|
dbRun("DELETE FROM production_contracts WHERE id = ?", [contractId]);
|
|
dbRun("DELETE FROM customer_contacts WHERE customer_id = ?", [customerId]);
|
|
dbRun("DELETE FROM customers WHERE id = ?", [customerId]);
|
|
for (const targetId of auditTargetIds) dbRun("DELETE FROM audit_logs WHERE target_id = ?", [targetId]);
|
|
for (const linkId of accessLinkIds) dbRun("DELETE FROM delivery_access_events WHERE link_id = ?", [linkId]);
|
|
for (const linkId of accessLinkIds) dbRun("DELETE FROM delivery_access_links WHERE id = ?", [linkId]);
|
|
for (const releaseId of releaseIds) {
|
|
dbRun("DELETE FROM audit_logs WHERE target_id = ?", [releaseId]);
|
|
dbRun("DELETE FROM delivery_releases WHERE id = ?", [releaseId]);
|
|
}
|
|
if (seeded) {
|
|
dbRun("DELETE FROM delivery_batch_items WHERE batch_id = ?", [batchId]);
|
|
dbRun("DELETE FROM delivery_batches WHERE id = ?", [batchId]);
|
|
}
|
|
for (const deliveryId of deliveryIds) {
|
|
dbRun("DELETE FROM audit_logs WHERE target_id = ?", [deliveryId]);
|
|
dbRun("DELETE FROM deliveries WHERE id = ?", [deliveryId]);
|
|
}
|
|
dbRun("DELETE FROM delivery_clearance_reports WHERE project_id = ?", [projectId]);
|
|
dbRun("DELETE FROM reviews WHERE project_id = ?", [projectId]);
|
|
if (projectCreated) {
|
|
dbRun("DELETE FROM shots WHERE episode_id IN (SELECT id FROM episodes WHERE season_id IN (SELECT id FROM seasons WHERE series_id = ?))", [`series-${projectId}`]);
|
|
dbRun("DELETE FROM episodes WHERE season_id IN (SELECT id FROM seasons WHERE series_id = ?)", [`series-${projectId}`]);
|
|
dbRun("DELETE FROM seasons WHERE series_id = ?", [`series-${projectId}`]);
|
|
dbRun("DELETE FROM series WHERE id = ?", [`series-${projectId}`]);
|
|
dbRun("DELETE FROM audit_logs WHERE project_id = ? OR metadata_json LIKE ?", [projectId, `%${projectId}%`]);
|
|
dbRun("DELETE FROM projects WHERE id = ?", [projectId]);
|
|
}
|
|
await rm(resolve(projectRoot, sourceRoot), { recursive: true, force: true });
|
|
await rm(resolve(projectRoot, "storage", "deliveries", projectId), { recursive: true, force: true });
|
|
await rm(resolve(projectRoot, "storage", "releases", projectId), { recursive: true, force: true });
|
|
}
|